mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-23 06:04:46 +08:00
feat: add ToggleGroup
This commit is contained in:
@@ -27,6 +27,8 @@
|
|||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-toggle": "^1.1.10",
|
||||||
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||||
"@t3-oss/env-nextjs": "^0.12.0",
|
"@t3-oss/env-nextjs": "^0.12.0",
|
||||||
|
|||||||
58
frontend/pnpm-lock.yaml
generated
58
frontend/pnpm-lock.yaml
generated
@@ -41,6 +41,12 @@ importers:
|
|||||||
'@radix-ui/react-slot':
|
'@radix-ui/react-slot':
|
||||||
specifier: ^1.2.4
|
specifier: ^1.2.4
|
||||||
version: 1.2.4(@types/react@19.2.8)(react@19.2.3)
|
version: 1.2.4(@types/react@19.2.8)(react@19.2.3)
|
||||||
|
'@radix-ui/react-toggle':
|
||||||
|
specifier: ^1.1.10
|
||||||
|
version: 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
|
'@radix-ui/react-toggle-group':
|
||||||
|
specifier: ^1.1.11
|
||||||
|
version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
'@radix-ui/react-tooltip':
|
'@radix-ui/react-tooltip':
|
||||||
specifier: ^1.2.8
|
specifier: ^1.2.8
|
||||||
version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
@@ -1088,6 +1094,32 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-toggle-group@1.1.11':
|
||||||
|
resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-toggle@1.1.10':
|
||||||
|
resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-tooltip@1.2.8':
|
'@radix-ui/react-tooltip@1.2.8':
|
||||||
resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
|
resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -5615,6 +5647,32 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.8
|
'@types/react': 19.2.8
|
||||||
|
|
||||||
|
'@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3)
|
||||||
|
'@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.3)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
|
'@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
|
'@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3)
|
||||||
|
react: 19.2.3
|
||||||
|
react-dom: 19.2.3(react@19.2.3)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.8
|
||||||
|
'@types/react-dom': 19.2.3(@types/react@19.2.8)
|
||||||
|
|
||||||
|
'@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3)
|
||||||
|
react: 19.2.3
|
||||||
|
react-dom: 19.2.3(react@19.2.3)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.8
|
||||||
|
'@types/react-dom': 19.2.3(@types/react@19.2.8)
|
||||||
|
|
||||||
'@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
'@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.3
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
|||||||
83
frontend/src/components/ui/toggle-group.tsx
Normal file
83
frontend/src/components/ui/toggle-group.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
||||||
|
import { type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { toggleVariants } from "@/components/ui/toggle";
|
||||||
|
|
||||||
|
const ToggleGroupContext = React.createContext<
|
||||||
|
VariantProps<typeof toggleVariants> & {
|
||||||
|
spacing?: number;
|
||||||
|
}
|
||||||
|
>({
|
||||||
|
size: "default",
|
||||||
|
variant: "default",
|
||||||
|
spacing: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
function ToggleGroup({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
spacing = 0,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
||||||
|
VariantProps<typeof toggleVariants> & {
|
||||||
|
spacing?: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ToggleGroupPrimitive.Root
|
||||||
|
data-slot="toggle-group"
|
||||||
|
data-variant={variant}
|
||||||
|
data-size={size}
|
||||||
|
data-spacing={spacing}
|
||||||
|
style={{ "--gap": spacing } as React.CSSProperties}
|
||||||
|
className={cn(
|
||||||
|
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ToggleGroupContext.Provider value={{ variant, size, spacing }}>
|
||||||
|
{children}
|
||||||
|
</ToggleGroupContext.Provider>
|
||||||
|
</ToggleGroupPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToggleGroupItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
||||||
|
VariantProps<typeof toggleVariants>) {
|
||||||
|
const context = React.useContext(ToggleGroupContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToggleGroupPrimitive.Item
|
||||||
|
data-slot="toggle-group-item"
|
||||||
|
data-variant={context.variant || variant}
|
||||||
|
data-size={context.size || size}
|
||||||
|
data-spacing={context.spacing}
|
||||||
|
className={cn(
|
||||||
|
toggleVariants({
|
||||||
|
variant: context.variant || variant,
|
||||||
|
size: context.size || size,
|
||||||
|
}),
|
||||||
|
"w-auto min-w-0 shrink-0 cursor-pointer px-3 focus:z-10 focus-visible:z-10",
|
||||||
|
"data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ToggleGroupPrimitive.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ToggleGroup, ToggleGroupItem };
|
||||||
47
frontend/src/components/ui/toggle.tsx
Normal file
47
frontend/src/components/ui/toggle.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
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",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-transparent",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-2 min-w-9",
|
||||||
|
sm: "h-8 px-1.5 min-w-8",
|
||||||
|
lg: "h-10 px-2.5 min-w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Toggle({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
||||||
|
VariantProps<typeof toggleVariants>) {
|
||||||
|
return (
|
||||||
|
<TogglePrimitive.Root
|
||||||
|
data-slot="toggle"
|
||||||
|
className={cn(toggleVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Toggle, toggleVariants }
|
||||||
@@ -4,8 +4,9 @@ import {
|
|||||||
SquareArrowOutUpRightIcon,
|
SquareArrowOutUpRightIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useMemo } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { Streamdown } from "streamdown";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Artifact,
|
Artifact,
|
||||||
@@ -22,13 +23,14 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||||
import { useArtifactContent } from "@/core/artifacts/hooks";
|
import { useArtifactContent } from "@/core/artifacts/hooks";
|
||||||
import { urlOfArtifact } from "@/core/artifacts/utils";
|
import { urlOfArtifact } from "@/core/artifacts/utils";
|
||||||
import { checkCodeFile, getFileName } from "@/core/utils/files";
|
import { checkCodeFile, getFileName } from "@/core/utils/files";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { useArtifacts } from "./context";
|
import { useArtifacts } from "./context";
|
||||||
import { FileViewer } from "./file-viewer";
|
|
||||||
|
|
||||||
export function ArtifactFileDetail({
|
export function ArtifactFileDetail({
|
||||||
className,
|
className,
|
||||||
@@ -50,23 +52,34 @@ export function ArtifactFileDetail({
|
|||||||
}
|
}
|
||||||
return filepathFromProps;
|
return filepathFromProps;
|
||||||
}, [filepathFromProps, isWriteFile]);
|
}, [filepathFromProps, isWriteFile]);
|
||||||
const { isCodeFile } = useMemo(() => {
|
const { isCodeFile, language } = useMemo(() => {
|
||||||
if (isWriteFile) {
|
if (isWriteFile) {
|
||||||
let language = checkCodeFile(filepath).language;
|
let language = checkCodeFile(filepath).language;
|
||||||
language ??= "markdown";
|
language ??= "text";
|
||||||
return { isCodeFile: true, language };
|
return { isCodeFile: true, language };
|
||||||
}
|
}
|
||||||
return checkCodeFile(filepath);
|
return checkCodeFile(filepath);
|
||||||
}, [filepath, isWriteFile]);
|
}, [filepath, isWriteFile]);
|
||||||
|
const previewable = useMemo(() => {
|
||||||
|
return (language === "html" && !isWriteFile) || language === "markdown";
|
||||||
|
}, [isWriteFile, language]);
|
||||||
const { content } = useArtifactContent({
|
const { content } = useArtifactContent({
|
||||||
threadId,
|
threadId,
|
||||||
filepath: filepathFromProps,
|
filepath: filepathFromProps,
|
||||||
enabled: isCodeFile && !isWriteFile,
|
enabled: isCodeFile && !isWriteFile,
|
||||||
});
|
});
|
||||||
|
const [viewMode, setViewMode] = useState<"code" | "preview">("code");
|
||||||
|
useEffect(() => {
|
||||||
|
if (previewable) {
|
||||||
|
setViewMode("preview");
|
||||||
|
} else {
|
||||||
|
setViewMode("code");
|
||||||
|
}
|
||||||
|
}, [previewable]);
|
||||||
return (
|
return (
|
||||||
<Artifact className={cn("rounded-none", className)}>
|
<Artifact className={cn("rounded-none", className)}>
|
||||||
<ArtifactHeader className="px-2">
|
<ArtifactHeader className="px-2">
|
||||||
<div>
|
<div className="flex items-center gap-2">
|
||||||
<ArtifactTitle>
|
<ArtifactTitle>
|
||||||
{isWriteFile ? (
|
{isWriteFile ? (
|
||||||
<div className="px-2">{getFileName(filepath)}</div>
|
<div className="px-2">{getFileName(filepath)}</div>
|
||||||
@@ -88,6 +101,23 @@ export function ArtifactFileDetail({
|
|||||||
)}
|
)}
|
||||||
</ArtifactTitle>
|
</ArtifactTitle>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex min-w-0 grow items-center justify-center">
|
||||||
|
{previewable && (
|
||||||
|
<ToggleGroup
|
||||||
|
className="mx-auto"
|
||||||
|
type="single"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
value={viewMode}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setViewMode(value as "code" | "preview")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ToggleGroupItem value="code">Code</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="preview">Preview</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ArtifactActions>
|
<ArtifactActions>
|
||||||
{!isWriteFile && (
|
{!isWriteFile && (
|
||||||
@@ -139,12 +169,57 @@ export function ArtifactFileDetail({
|
|||||||
</div>
|
</div>
|
||||||
</ArtifactHeader>
|
</ArtifactHeader>
|
||||||
<ArtifactContent className="p-0">
|
<ArtifactContent className="p-0">
|
||||||
<FileViewer
|
{previewable && viewMode === "preview" && (
|
||||||
className="size-full"
|
<ArtifactFilePreview
|
||||||
|
filepath={filepath}
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
filepath={filepathFromProps}
|
content={content}
|
||||||
|
language={language ?? "text"}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
{isCodeFile && viewMode === "code" && (
|
||||||
|
<Textarea
|
||||||
|
className="size-full resize-none rounded-none border-none"
|
||||||
|
readOnly
|
||||||
|
value={content ?? ""}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isCodeFile && (
|
||||||
|
<iframe
|
||||||
|
className="size-full"
|
||||||
|
src={urlOfArtifact({ filepath, threadId })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</ArtifactContent>
|
</ArtifactContent>
|
||||||
</Artifact>
|
</Artifact>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ArtifactFilePreview({
|
||||||
|
filepath,
|
||||||
|
threadId,
|
||||||
|
content,
|
||||||
|
language,
|
||||||
|
}: {
|
||||||
|
filepath: string;
|
||||||
|
threadId: string;
|
||||||
|
content: string;
|
||||||
|
language: string;
|
||||||
|
}) {
|
||||||
|
if (language === "markdown") {
|
||||||
|
return (
|
||||||
|
<div className="size-full px-4">
|
||||||
|
<Streamdown className="size-full">{content ?? ""}</Streamdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (language === "html") {
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
className="size-full"
|
||||||
|
src={urlOfArtifact({ filepath, threadId })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
import { useMemo } from "react";
|
|
||||||
import type { BundledLanguage } from "shiki";
|
|
||||||
|
|
||||||
import { CodeBlock } from "@/components/ai-elements/code-block";
|
|
||||||
import { useArtifactContent } from "@/core/artifacts/hooks";
|
|
||||||
import { urlOfArtifact } from "@/core/artifacts/utils";
|
|
||||||
import { checkCodeFile } from "@/core/utils/files";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export function FileViewer({
|
|
||||||
className,
|
|
||||||
filepath,
|
|
||||||
threadId,
|
|
||||||
}: {
|
|
||||||
className?: string;
|
|
||||||
filepath: string;
|
|
||||||
threadId: string;
|
|
||||||
}) {
|
|
||||||
const isWriteFile = useMemo(() => {
|
|
||||||
return filepath.startsWith("write-file:");
|
|
||||||
}, [filepath]);
|
|
||||||
const { isCodeFile, language } = useMemo(() => {
|
|
||||||
if (isWriteFile) {
|
|
||||||
const url = new URL(filepath);
|
|
||||||
const path = decodeURIComponent(url.pathname);
|
|
||||||
return checkCodeFile(path);
|
|
||||||
}
|
|
||||||
return checkCodeFile(filepath);
|
|
||||||
}, [filepath, isWriteFile]);
|
|
||||||
if (isWriteFile || (isCodeFile && language !== "html")) {
|
|
||||||
return (
|
|
||||||
<CodeFileView
|
|
||||||
language={language ?? "markdown"}
|
|
||||||
filepath={filepath}
|
|
||||||
threadId={threadId}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className={cn("size-full border-none", className)}>
|
|
||||||
<iframe
|
|
||||||
className={cn("size-full border-none", className)}
|
|
||||||
src={urlOfArtifact({ filepath, threadId })}
|
|
||||||
></iframe>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CodeFileView({
|
|
||||||
language,
|
|
||||||
filepath,
|
|
||||||
threadId,
|
|
||||||
}: {
|
|
||||||
language: BundledLanguage;
|
|
||||||
filepath: string;
|
|
||||||
threadId: string;
|
|
||||||
}) {
|
|
||||||
const { content: code } = useArtifactContent({
|
|
||||||
filepath,
|
|
||||||
threadId,
|
|
||||||
});
|
|
||||||
if (code) {
|
|
||||||
return (
|
|
||||||
<CodeBlock
|
|
||||||
className="size-full rounded-none border-none"
|
|
||||||
language={language}
|
|
||||||
code={code}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ import { useMemo } from "react";
|
|||||||
|
|
||||||
import { useThread } from "@/components/workspace/messages/context";
|
import { useThread } from "@/components/workspace/messages/context";
|
||||||
|
|
||||||
import { loadArtifactContent } from "./loader";
|
import { loadArtifactContent, loadArtifactContentFromToolCall } from "./loader";
|
||||||
|
|
||||||
export function useArtifactContent({
|
export function useArtifactContent({
|
||||||
filepath,
|
filepath,
|
||||||
@@ -20,25 +20,10 @@ export function useArtifactContent({
|
|||||||
const { thread } = useThread();
|
const { thread } = useThread();
|
||||||
const content = useMemo(() => {
|
const content = useMemo(() => {
|
||||||
if (isWriteFile) {
|
if (isWriteFile) {
|
||||||
const url = new URL(filepath);
|
return loadArtifactContentFromToolCall({ url: filepath, thread });
|
||||||
const toolCallId = url.searchParams.get("tool_call_id");
|
|
||||||
const messageId = url.searchParams.get("message_id");
|
|
||||||
if (messageId && toolCallId) {
|
|
||||||
const message = thread.messages.find(
|
|
||||||
(message) => message.id === messageId,
|
|
||||||
);
|
|
||||||
if (message?.type === "ai" && message.tool_calls) {
|
|
||||||
const toolCall = message.tool_calls.find(
|
|
||||||
(toolCall) => toolCall.id === toolCallId,
|
|
||||||
);
|
|
||||||
if (toolCall) {
|
|
||||||
return toolCall.args.content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [filepath, isWriteFile, thread.messages]);
|
}, [filepath, isWriteFile, thread]);
|
||||||
const { data, isLoading, error } = useQuery({
|
const { data, isLoading, error } = useQuery({
|
||||||
queryKey: ["artifact", filepath, threadId],
|
queryKey: ["artifact", filepath, threadId],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import type { UseStream } from "@langchain/langgraph-sdk/react";
|
||||||
|
|
||||||
|
import type { AgentThreadState } from "../threads";
|
||||||
|
|
||||||
import { urlOfArtifact } from "./utils";
|
import { urlOfArtifact } from "./utils";
|
||||||
|
|
||||||
export async function loadArtifactContent({
|
export async function loadArtifactContent({
|
||||||
@@ -12,3 +16,26 @@ export async function loadArtifactContent({
|
|||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function loadArtifactContentFromToolCall({
|
||||||
|
url: urlString,
|
||||||
|
thread,
|
||||||
|
}: {
|
||||||
|
url: string;
|
||||||
|
thread: UseStream<AgentThreadState>;
|
||||||
|
}) {
|
||||||
|
const url = new URL(urlString);
|
||||||
|
const toolCallId = url.searchParams.get("tool_call_id");
|
||||||
|
const messageId = url.searchParams.get("message_id");
|
||||||
|
if (messageId && toolCallId) {
|
||||||
|
const message = thread.messages.find((message) => message.id === messageId);
|
||||||
|
if (message?.type === "ai" && message.tool_calls) {
|
||||||
|
const toolCall = message.tool_calls.find(
|
||||||
|
(toolCall) => toolCall.id === toolCallId,
|
||||||
|
);
|
||||||
|
if (toolCall) {
|
||||||
|
return toolCall.args.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import type { BundledLanguage } from "shiki";
|
const extensionMap: Record<string, string> = {
|
||||||
|
// Text
|
||||||
|
txt: "text",
|
||||||
|
csv: "csv",
|
||||||
|
log: "text",
|
||||||
|
conf: "text",
|
||||||
|
config: "text",
|
||||||
|
properties: "text",
|
||||||
|
props: "text",
|
||||||
|
|
||||||
const extensionMap: Record<string, BundledLanguage> = {
|
|
||||||
// JavaScript/TypeScript ecosystem
|
// JavaScript/TypeScript ecosystem
|
||||||
js: "javascript",
|
js: "javascript",
|
||||||
jsx: "jsx",
|
jsx: "jsx",
|
||||||
@@ -137,14 +144,14 @@ export function getFileExtension(filepath: string) {
|
|||||||
export function checkCodeFile(
|
export function checkCodeFile(
|
||||||
filepath: string,
|
filepath: string,
|
||||||
):
|
):
|
||||||
| { isCodeFile: true; language: BundledLanguage }
|
| { isCodeFile: true; language: string }
|
||||||
| { isCodeFile: false; language: null } {
|
| { isCodeFile: false; language: null } {
|
||||||
const extension = getFileExtension(filepath);
|
const extension = getFileExtension(filepath);
|
||||||
const isCodeFile = extension in extensionMap;
|
const isCodeFile = extension in extensionMap;
|
||||||
if (isCodeFile) {
|
if (isCodeFile) {
|
||||||
return {
|
return {
|
||||||
isCodeFile: true,
|
isCodeFile: true,
|
||||||
language: extensionMap[extension] as unknown as BundledLanguage,
|
language: extensionMap[extension] ?? "text",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user