mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-16 03:14:45 +08:00
feat: implement i18n
This commit is contained in:
@@ -27,6 +27,7 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import { useArtifactContent } from "@/core/artifacts/hooks";
|
||||
import { urlOfArtifact } from "@/core/artifacts/utils";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { checkCodeFile, getFileName } from "@/core/utils/files";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -41,6 +42,7 @@ export function ArtifactFileDetail({
|
||||
filepath: string;
|
||||
threadId: string;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const { artifacts, setOpen, select } = useArtifacts();
|
||||
const isWriteFile = useMemo(() => {
|
||||
return filepathFromProps.startsWith("write-file:");
|
||||
@@ -124,26 +126,26 @@ export function ArtifactFileDetail({
|
||||
<a href={urlOfArtifact({ filepath, threadId })} target="_blank">
|
||||
<ArtifactAction
|
||||
icon={SquareArrowOutUpRightIcon}
|
||||
label="Open in new window"
|
||||
tooltip="Open in new window"
|
||||
label={t.common.openInNewWindow}
|
||||
tooltip={t.common.openInNewWindow}
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
{isCodeFile && (
|
||||
<ArtifactAction
|
||||
icon={CopyIcon}
|
||||
label="Copy"
|
||||
label={t.clipboard.copyToClipboard}
|
||||
disabled={!content}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content ?? "");
|
||||
toast.success("Copied to clipboard");
|
||||
toast.success(t.clipboard.copiedToClipboard);
|
||||
} catch (error) {
|
||||
toast.error("Failed to copy to clipboard");
|
||||
console.error(error);
|
||||
}
|
||||
}}
|
||||
tooltip="Copy content to clipboard"
|
||||
tooltip={t.clipboard.copyToClipboard}
|
||||
/>
|
||||
)}
|
||||
{!isWriteFile && (
|
||||
@@ -153,17 +155,16 @@ export function ArtifactFileDetail({
|
||||
>
|
||||
<ArtifactAction
|
||||
icon={DownloadIcon}
|
||||
label="Download"
|
||||
onClick={() => console.log("Download")}
|
||||
tooltip="Download file"
|
||||
label={t.common.download}
|
||||
tooltip={t.common.download}
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
<ArtifactAction
|
||||
icon={XIcon}
|
||||
label="Close"
|
||||
label={t.common.close}
|
||||
onClick={() => setOpen(false)}
|
||||
tooltip="Close"
|
||||
tooltip={t.common.close}
|
||||
/>
|
||||
</ArtifactActions>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { urlOfArtifact } from "@/core/artifacts/utils";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { getFileExtensionDisplayName, getFileName } from "@/core/utils/files";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -24,6 +25,7 @@ export function ArtifactFileList({
|
||||
files: string[];
|
||||
threadId: string;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const { select: selectArtifact, setOpen } = useArtifacts();
|
||||
const handleClick = useCallback(
|
||||
(filepath: string) => {
|
||||
@@ -57,7 +59,7 @@ export function ArtifactFileList({
|
||||
>
|
||||
<Button variant="ghost">
|
||||
<DownloadIcon className="size-4" />
|
||||
Download
|
||||
{t.common.download}
|
||||
</Button>
|
||||
</a>
|
||||
</CardAction>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { useCallback, useState, type ComponentProps } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
|
||||
import { Tooltip } from "./tooltip";
|
||||
|
||||
@@ -11,6 +12,7 @@ export function CopyButton({
|
||||
}: ComponentProps<typeof Button> & {
|
||||
clipboardData: string;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const handleCopy = useCallback(() => {
|
||||
void navigator.clipboard.writeText(clipboardData);
|
||||
@@ -18,7 +20,7 @@ export function CopyButton({
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}, [clipboardData]);
|
||||
return (
|
||||
<Tooltip content="Copy to clipboard">
|
||||
<Tooltip content={t.clipboard.copyToClipboard}>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import type { ChatStatus } from "ai";
|
||||
import { CheckIcon, LightbulbIcon, LightbulbOffIcon } from "lucide-react";
|
||||
import { useCallback, useMemo, useState, type ComponentProps } from "react";
|
||||
@@ -11,6 +13,7 @@ import {
|
||||
PromptInputTextarea,
|
||||
type PromptInputMessage,
|
||||
} from "@/components/ai-elements/prompt-input";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { useModels } from "@/core/models/hooks";
|
||||
import type { AgentThreadContext } from "@/core/threads";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -40,11 +43,11 @@ export function InputBox({
|
||||
assistantId?: string | null;
|
||||
status?: ChatStatus;
|
||||
context: Omit<AgentThreadContext, "thread_id">;
|
||||
showWelcome?: boolean;
|
||||
onContextChange?: (context: Omit<AgentThreadContext, "thread_id">) => void;
|
||||
onSubmit?: (message: PromptInputMessage) => void;
|
||||
onStop?: () => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const [modelDialogOpen, setModelDialogOpen] = useState(false);
|
||||
const { models } = useModels();
|
||||
const selectedModel = useMemo(
|
||||
@@ -97,7 +100,7 @@ export function InputBox({
|
||||
<PromptInputBody>
|
||||
<PromptInputTextarea
|
||||
className={cn("size-full")}
|
||||
placeholder="How can I assist you today?"
|
||||
placeholder={t.inputBox.placeholder}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
</PromptInputBody>
|
||||
@@ -107,13 +110,17 @@ export function InputBox({
|
||||
content={
|
||||
context.thinking_enabled ? (
|
||||
<div className="tex-sm flex flex-col gap-1">
|
||||
<div>Thinking is enabled</div>
|
||||
<div className="opacity-50">Click to disable thinking</div>
|
||||
<div>{t.inputBox.thinkingEnabled}</div>
|
||||
<div className="opacity-50">
|
||||
{t.inputBox.clickToDisableThinking}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="tex-sm flex flex-col gap-1">
|
||||
<div>Thinking is disabled</div>
|
||||
<div className="opacity-50">Click to enable thinking</div>
|
||||
<div>{t.inputBox.thinkingDisabled}</div>
|
||||
<div className="opacity-50">
|
||||
{t.inputBox.clickToEnableThinking}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -142,7 +149,7 @@ export function InputBox({
|
||||
</PromptInputButton>
|
||||
</ModelSelectorTrigger>
|
||||
<ModelSelectorContent>
|
||||
<ModelSelectorInput placeholder="Search models..." />
|
||||
<ModelSelectorInput placeholder={t.inputBox.searchModels} />
|
||||
<ModelSelectorList>
|
||||
{models.map((m) => (
|
||||
<ModelSelectorItem
|
||||
|
||||
@@ -21,8 +21,10 @@ import {
|
||||
ChainOfThoughtSearchResults,
|
||||
ChainOfThoughtStep,
|
||||
} from "@/components/ai-elements/chain-of-thought";
|
||||
import { CodeBlock } from "@/components/ai-elements/code-block";
|
||||
import { MessageResponse } from "@/components/ai-elements/message";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import {
|
||||
extractReasoningContentFromMessage,
|
||||
findToolCallResult,
|
||||
@@ -33,7 +35,6 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
import { useArtifacts } from "../artifacts";
|
||||
import { FlipDisplay } from "../flip-display";
|
||||
import { CodeBlock } from "@/components/ai-elements/code-block";
|
||||
|
||||
export function MessageGroup({
|
||||
className,
|
||||
@@ -44,6 +45,7 @@ export function MessageGroup({
|
||||
messages: Message[];
|
||||
isLoading?: boolean;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const [showAbove, setShowAbove] = useState(false);
|
||||
const [showLastThinking, setShowLastThinking] = useState(false);
|
||||
const steps = useMemo(() => convertToSteps(messages), [messages]);
|
||||
@@ -84,8 +86,8 @@ export function MessageGroup({
|
||||
label={
|
||||
<span className="opacity-60">
|
||||
{showAbove
|
||||
? "Less steps"
|
||||
: `${aboveLastToolCallSteps.length} more step${aboveLastToolCallSteps.length === 1 ? "" : "s"}`}
|
||||
? t.toolCalls.lessSteps
|
||||
: t.toolCalls.moreSteps(aboveLastToolCallSteps.length)}
|
||||
</span>
|
||||
}
|
||||
icon={
|
||||
@@ -134,7 +136,7 @@ export function MessageGroup({
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<ChainOfThoughtStep
|
||||
className="font-normal"
|
||||
label="Thinking"
|
||||
label={t.common.thinking}
|
||||
icon={LightbulbIcon}
|
||||
></ChainOfThoughtStep>
|
||||
<div>
|
||||
@@ -178,16 +180,12 @@ function ToolCall({
|
||||
args: Record<string, unknown>;
|
||||
result?: string | Record<string, unknown>;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const { select, setOpen } = useArtifacts();
|
||||
if (name === "web_search") {
|
||||
let label: React.ReactNode = "Search for related information";
|
||||
let label: React.ReactNode = t.toolCalls.searchForRelatedInfo;
|
||||
if (typeof args.query === "string") {
|
||||
label = (
|
||||
<div>
|
||||
Search on the web for{" "}
|
||||
<span className="font-bold">"{args.query}"</span>
|
||||
</div>
|
||||
);
|
||||
label = t.toolCalls.searchOnWebFor(args.query);
|
||||
}
|
||||
return (
|
||||
<ChainOfThoughtStep key={id} label={label} icon={SearchIcon}>
|
||||
@@ -217,7 +215,7 @@ function ToolCall({
|
||||
<ChainOfThoughtStep
|
||||
key={id}
|
||||
className="cursor-pointer"
|
||||
label="View web page"
|
||||
label={t.toolCalls.viewWebPage}
|
||||
icon={GlobeIcon}
|
||||
onClick={() => {
|
||||
window.open(url, "_blank");
|
||||
@@ -236,7 +234,7 @@ function ToolCall({
|
||||
let description: string | undefined = (args as { description: string })
|
||||
?.description;
|
||||
if (!description) {
|
||||
description = "List folder";
|
||||
description = t.toolCalls.listFolder;
|
||||
}
|
||||
const path: string | undefined = (args as { path: string })?.path;
|
||||
return (
|
||||
@@ -250,7 +248,7 @@ function ToolCall({
|
||||
let description: string | undefined = (args as { description: string })
|
||||
?.description;
|
||||
if (!description) {
|
||||
description = "Read file";
|
||||
description = t.toolCalls.readFile;
|
||||
}
|
||||
const path: string | undefined = (args as { path: string })?.path;
|
||||
return (
|
||||
@@ -264,7 +262,7 @@ function ToolCall({
|
||||
let description: string | undefined = (args as { description: string })
|
||||
?.description;
|
||||
if (!description) {
|
||||
description = "Write file";
|
||||
description = t.toolCalls.writeFile;
|
||||
}
|
||||
const path: string | undefined = (args as { path: string })?.path;
|
||||
return (
|
||||
@@ -291,7 +289,7 @@ function ToolCall({
|
||||
const description: string | undefined = (args as { description: string })
|
||||
?.description;
|
||||
if (!description) {
|
||||
return "Execute command";
|
||||
return t.toolCalls.executeCommand;
|
||||
}
|
||||
const command: string | undefined = (args as { command: string })?.command;
|
||||
return (
|
||||
@@ -312,7 +310,11 @@ function ToolCall({
|
||||
);
|
||||
} else if (name === "present_files") {
|
||||
return (
|
||||
<ChainOfThoughtStep key={id} label="Present files" icon={FileTextIcon}>
|
||||
<ChainOfThoughtStep
|
||||
key={id}
|
||||
label={t.toolCalls.presentFiles}
|
||||
icon={FileTextIcon}
|
||||
>
|
||||
<ChainOfThoughtSearchResult>
|
||||
{Array.isArray((args as { filepaths: string[] }).filepaths) &&
|
||||
(args as { filepaths: string[] }).filepaths.map(
|
||||
@@ -330,7 +332,7 @@ function ToolCall({
|
||||
return (
|
||||
<ChainOfThoughtStep
|
||||
key={id}
|
||||
label="Need your help"
|
||||
label={t.toolCalls.needYourHelp}
|
||||
icon={MessageCircleQuestionMarkIcon}
|
||||
></ChainOfThoughtStep>
|
||||
);
|
||||
@@ -340,13 +342,7 @@ function ToolCall({
|
||||
return (
|
||||
<ChainOfThoughtStep
|
||||
key={id}
|
||||
label={
|
||||
description ?? (
|
||||
<div>
|
||||
Use "<span className="font-bold">{name}</span>" tool
|
||||
</div>
|
||||
)
|
||||
}
|
||||
label={description ?? t.toolCalls.useTool(name)}
|
||||
icon={WrenchIcon}
|
||||
></ChainOfThoughtStep>
|
||||
);
|
||||
|
||||
@@ -20,10 +20,12 @@ import {
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { useDeleteThread, useThreads } from "@/core/threads/hooks";
|
||||
import { pathOfThread, titleOfThread } from "@/core/threads/utils";
|
||||
|
||||
export function RecentChatList() {
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>();
|
||||
@@ -52,7 +54,7 @@ export function RecentChatList() {
|
||||
}
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Recent chats</SidebarGroupLabel>
|
||||
<SidebarGroupLabel>{t.sidebar.recentChats}</SidebarGroupLabel>
|
||||
<SidebarGroupContent className="group-data-[collapsible=icon]:pointer-events-none group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0">
|
||||
<SidebarMenu>
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
@@ -78,7 +80,7 @@ export function RecentChatList() {
|
||||
className="bg-background/50 hover:bg-background"
|
||||
>
|
||||
<MoreHorizontal />
|
||||
<span className="sr-only">More</span>
|
||||
<span className="sr-only">{t.common.more}</span>
|
||||
</SidebarMenuAction>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
@@ -90,7 +92,7 @@ export function RecentChatList() {
|
||||
onSelect={() => handleDelete(thread.thread_id)}
|
||||
>
|
||||
<Trash2 className="text-muted-foreground" />
|
||||
<span>Delete</span>
|
||||
<span>{t.common.delete}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
0
frontend/src/components/workspace/settings/index.ts
Normal file
0
frontend/src/components/workspace/settings/index.ts
Normal file
@@ -1,6 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Welcome({ className }: { className?: string }) {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -8,17 +12,13 @@ export function Welcome({ className }: { className?: string }) {
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="text-2xl font-bold">👋 Hello, again!</div>
|
||||
<div className="text-2xl font-bold">{t.welcome.greeting}</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
<p>
|
||||
Welcome to 🦌 DeerFlow, an open source super agent. With built-in and
|
||||
custom
|
||||
</p>
|
||||
<p>
|
||||
skills, DeerFlow helps you search on the web, analyze data, and
|
||||
generate
|
||||
</p>{" "}
|
||||
<p>artifacts like slides, web pages and do almost anything.</p>
|
||||
{t.welcome.description.includes("\n") ? (
|
||||
<pre className="whitespace-pre">{t.welcome.description}</pre>
|
||||
) : (
|
||||
<p>{t.welcome.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { GithubIcon } from "./github-icon";
|
||||
@@ -34,6 +35,7 @@ export function WorkspaceHeader({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"header">) {
|
||||
const { t } = useI18n();
|
||||
const pathname = usePathname();
|
||||
const segments = useMemo(() => {
|
||||
const parts = pathname?.split("/") || [];
|
||||
@@ -56,7 +58,7 @@ export function WorkspaceHeader({
|
||||
<BreadcrumbItem className="hidden md:block">
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href={`/${segments[0]}`}>
|
||||
{nameOfSegment(segments[0])}
|
||||
{nameOfSegment(segments[0], t)}
|
||||
</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
@@ -68,12 +70,12 @@ export function WorkspaceHeader({
|
||||
{segments.length >= 2 ? (
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href={`/${segments[0]}/${segments[1]}`}>
|
||||
{nameOfSegment(segments[1])}
|
||||
{nameOfSegment(segments[1], t)}
|
||||
</Link>
|
||||
</BreadcrumbLink>
|
||||
) : (
|
||||
<BreadcrumbPage>
|
||||
{nameOfSegment(segments[1])}
|
||||
{nameOfSegment(segments[1], t)}
|
||||
</BreadcrumbPage>
|
||||
)}
|
||||
</BreadcrumbItem>
|
||||
@@ -89,7 +91,7 @@ export function WorkspaceHeader({
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
<div className="pr-4">
|
||||
<Tooltip content="DeerFlow on Github">
|
||||
<Tooltip content={t.workspace.githubTooltip}>
|
||||
<a
|
||||
href="https://github.com/bytedance/deer-flow"
|
||||
target="_blank"
|
||||
@@ -122,7 +124,12 @@ export function WorkspaceBody({
|
||||
);
|
||||
}
|
||||
|
||||
function nameOfSegment(segment: string | undefined) {
|
||||
if (!segment) return "Home";
|
||||
function nameOfSegment(
|
||||
segment: string | undefined,
|
||||
t: ReturnType<typeof useI18n>["t"],
|
||||
) {
|
||||
if (!segment) return t.common.home;
|
||||
if (segment === "workspace") return t.breadcrumb.workspace;
|
||||
if (segment === "chats") return t.breadcrumb.chats;
|
||||
return segment[0]?.toUpperCase() + segment.slice(1);
|
||||
}
|
||||
|
||||
@@ -11,9 +11,11 @@ import {
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function WorkspaceHeader({ className }: { className?: string }) {
|
||||
const { t } = useI18n();
|
||||
const { state } = useSidebar();
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
@@ -48,7 +50,7 @@ export function WorkspaceHeader({ className }: { className?: string }) {
|
||||
>
|
||||
<Link className="text-muted-foreground" href="/workspace/chats/new">
|
||||
<MessageSquarePlus size={16} />
|
||||
<span>New chat</span>
|
||||
<span>{t.sidebar.newChat}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
||||
@@ -10,8 +10,10 @@ import {
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
|
||||
export function WorkspaceNavMenu() {
|
||||
const { t } = useI18n();
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<SidebarGroup className="pt-1">
|
||||
@@ -20,7 +22,7 @@ export function WorkspaceNavMenu() {
|
||||
<SidebarMenuButton isActive={pathname === "/workspace/chats"} asChild>
|
||||
<Link className="text-muted-foreground" href="/workspace/chats">
|
||||
<MessagesSquare />
|
||||
<span>Chats</span>
|
||||
<span>{t.sidebar.chats}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { SettingsIcon } from "lucide-react";
|
||||
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarHeader,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarRail,
|
||||
SidebarMenu,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuButton,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
|
||||
import { RecentChatList } from "./recent-chat-list";
|
||||
import { WorkspaceHeader } from "./workspace-header";
|
||||
@@ -13,6 +21,7 @@ import { WorkspaceNavMenu } from "./workspace-nav-menu";
|
||||
export function WorkspaceSidebar({
|
||||
...props
|
||||
}: React.ComponentProps<typeof Sidebar>) {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<Sidebar variant="sidebar" collapsible="icon" {...props}>
|
||||
<SidebarHeader className="py-0">
|
||||
@@ -22,7 +31,22 @@ export function WorkspaceSidebar({
|
||||
<WorkspaceNavMenu />
|
||||
<RecentChatList />
|
||||
</SidebarContent>
|
||||
<SidebarFooter></SidebarFooter>
|
||||
<SidebarFooter>
|
||||
<SidebarGroup className="px-0">
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<div className="text-muted-foreground cursor-pointer">
|
||||
<SettingsIcon size={16} />
|
||||
<span>{t.common.settings}</span>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarFooter>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user