style: lint prettier

This commit is contained in:
肖应宇 2026-04-20 10:34:57 +08:00
parent d82ac30b93
commit 170b5484c9
22 changed files with 406 additions and 288 deletions

View File

@ -1,5 +1,6 @@
"use client"; "use client";
import { Ticker } from "@tombcato/smart-ticker";
import { FilesIcon, ListTodoIcon, XIcon } from "lucide-react"; import { FilesIcon, ListTodoIcon, XIcon } from "lucide-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@ -22,6 +23,7 @@ import {
} from "@/components/workspace/artifacts"; } from "@/components/workspace/artifacts";
import { useThreadChat } from "@/components/workspace/chats"; import { useThreadChat } from "@/components/workspace/chats";
// import { DevTodoList } from "@/components/workspace/dev-todo-list"; // import { DevTodoList } from "@/components/workspace/dev-todo-list";
import { IframeTestPanel } from "@/components/workspace/iframe-test-panel";
import { InputBox } from "@/components/workspace/input-box"; import { InputBox } from "@/components/workspace/input-box";
import { MessageList } from "@/components/workspace/messages"; import { MessageList } from "@/components/workspace/messages";
import { ThreadContext } from "@/components/workspace/messages/context"; import { ThreadContext } from "@/components/workspace/messages/context";
@ -39,8 +41,7 @@ import { textOfMessage } from "@/core/threads/utils";
import { env } from "@/env"; import { env } from "@/env";
import { useSelectedSkillListener } from "@/hooks/use-selected-skill-listener"; import { useSelectedSkillListener } from "@/hooks/use-selected-skill-listener";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { IframeTestPanel } from "@/components/workspace/iframe-test-panel";
import { Ticker } from "@tombcato/smart-ticker";
import "@tombcato/smart-ticker/style.css"; import "@tombcato/smart-ticker/style.css";
import motivationSlogans from "./motivation-slogans.json"; import motivationSlogans from "./motivation-slogans.json";
@ -141,7 +142,7 @@ export default function ChatPage() {
if (initializedThreadRef.current === safeThreadId) return; if (initializedThreadRef.current === safeThreadId) return;
initializedThreadRef.current = safeThreadId; initializedThreadRef.current = safeThreadId;
void apiClient.threads void apiClient.threads
// TODO: 先注释先删除再创建的逻辑 // TODO: 先注释先删除再创建的逻辑
// .delete(safeThreadId) // .delete(safeThreadId)
// .catch(() => undefined) // .catch(() => undefined)
// .then(() => // .then(() =>
@ -489,9 +490,7 @@ export default function ChatPage() {
/> />
) : ( ) : (
<div className="relative flex size-full justify-center px-[20px]"> <div className="relative flex size-full justify-center px-[20px]">
<div className="z-30"> <div className="z-30"></div>
</div>
{thread.values.artifacts?.length === 0 ? ( {thread.values.artifacts?.length === 0 ? (
<ConversationEmptyState <ConversationEmptyState
icon={<FilesIcon />} icon={<FilesIcon />}
@ -500,20 +499,20 @@ export default function ChatPage() {
/> />
) : ( ) : (
<div className="flex size-full max-w-(--container-width-sm) flex-col justify-center"> <div className="flex size-full max-w-(--container-width-sm) flex-col justify-center">
<header className="shrink-0 flex justify-between items-center border-b "> <header className="flex shrink-0 items-center justify-between border-b">
<h2 className="text-[14px] h-[58px] leading-[58px] font-bold text-[#333333]"> <h2 className="h-[58px] text-[14px] leading-[58px] font-bold text-[#333333]">
<span>{t.common.artifacts}</span> <span>{t.common.artifacts}</span>
</h2> </h2>
<Button <Button
data-testid="artifacts-panel-close" data-testid="artifacts-panel-close"
size="icon-sm" size="icon-sm"
variant="ghost" variant="ghost"
onClick={() => { onClick={() => {
setArtifactsOpen(false); setArtifactsOpen(false);
}} }}
> >
<XIcon /> <XIcon />
</Button> </Button>
</header> </header>
<main className="min-h-0 grow overflow-auto"> <main className="min-h-0 grow overflow-auto">
<ArtifactFileList <ArtifactFileList
@ -655,7 +654,9 @@ export default function ChatPage() {
<DevDialogContent> <DevDialogContent>
<DevDialogHeader> <DevDialogHeader>
<DevDialogTitle> <DevDialogTitle>
{selectedSkillError?.title ?? t.chatPage.selectedSkillLoadFailed} {" "}
{selectedSkillError?.title ??
t.chatPage.selectedSkillLoadFailed}
</DevDialogTitle> </DevDialogTitle>
</DevDialogHeader> </DevDialogHeader>
<p className="text-muted-foreground text-sm"> <p className="text-muted-foreground text-sm">

View File

@ -36,7 +36,7 @@ export const Message = ({
"group flex w-full flex-col gap-2", "group flex w-full flex-col gap-2",
from === "user" from === "user"
? cn("is-user ml-auto justify-end", !isFirstInSession && "mt-6") ? cn("is-user ml-auto justify-end", !isFirstInSession && "mt-6")
: "is-assistant bg-white rounded-[10px] p-4", : "is-assistant rounded-[10px] bg-white p-4",
className, className,
)} )}
{...props} {...props}

View File

@ -350,19 +350,19 @@ export function PromptInputAttachment({
/> />
</svg> </svg>
{/* 删除按钮 - 右上角 */} {/* 删除按钮 - 右上角 */}
<button <button
aria-label={t.common.removeAttachment} aria-label={t.common.removeAttachment}
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" 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) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
if (onRemove) { if (onRemove) {
onRemove(); onRemove();
return; return;
} }
attachments.remove(data.id); attachments.remove(data.id);
}} }}
type="button" type="button"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="8" width="8"

View File

@ -205,7 +205,7 @@ export const ReasoningContent = memo(
{...props} {...props}
> >
{isStreaming ? ( {isStreaming ? (
<div className="whitespace-pre-wrap break-words">{children}</div> <div className="break-words whitespace-pre-wrap">{children}</div>
) : ( ) : (
<Streamdown <Streamdown
isAnimating={false} isAnimating={false}

View File

@ -1,15 +1,15 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu" import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function ContextMenu({ function ContextMenu({
...props ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) { }: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} /> return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
} }
function ContextMenuTrigger({ function ContextMenuTrigger({
@ -17,7 +17,7 @@ function ContextMenuTrigger({
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) { }: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
return ( return (
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} /> <ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
) );
} }
function ContextMenuGroup({ function ContextMenuGroup({
@ -25,7 +25,7 @@ function ContextMenuGroup({
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) { }: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
return ( return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} /> <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
) );
} }
function ContextMenuPortal({ function ContextMenuPortal({
@ -33,13 +33,13 @@ function ContextMenuPortal({
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) { }: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return ( return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} /> <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
) );
} }
function ContextMenuSub({ function ContextMenuSub({
...props ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) { }: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} /> return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
} }
function ContextMenuRadioGroup({ function ContextMenuRadioGroup({
@ -50,7 +50,7 @@ function ContextMenuRadioGroup({
data-slot="context-menu-radio-group" data-slot="context-menu-radio-group"
{...props} {...props}
/> />
) );
} }
function ContextMenuSubTrigger({ function ContextMenuSubTrigger({
@ -59,22 +59,22 @@ function ContextMenuSubTrigger({
children, children,
...props ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & { }: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<ContextMenuPrimitive.SubTrigger <ContextMenuPrimitive.SubTrigger
data-slot="context-menu-sub-trigger" data-slot="context-menu-sub-trigger"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground", "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 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} {...props}
> >
{children} {children}
<ChevronRightIcon className="ml-auto" /> <ChevronRightIcon className="ml-auto" />
</ContextMenuPrimitive.SubTrigger> </ContextMenuPrimitive.SubTrigger>
) );
} }
function ContextMenuSubContent({ function ContextMenuSubContent({
@ -85,12 +85,12 @@ function ContextMenuSubContent({
<ContextMenuPrimitive.SubContent <ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content" data-slot="context-menu-sub-content"
className={cn( className={cn(
"z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", "bg-popover text-popover-foreground 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=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function ContextMenuContent({ function ContextMenuContent({
@ -102,13 +102,13 @@ function ContextMenuContent({
<ContextMenuPrimitive.Content <ContextMenuPrimitive.Content
data-slot="context-menu-content" data-slot="context-menu-content"
className={cn( className={cn(
"z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-0 text-popover-foreground shadow-md 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=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", "bg-popover text-popover-foreground 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=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-0 shadow-md",
className className,
)} )}
{...props} {...props}
/> />
</ContextMenuPrimitive.Portal> </ContextMenuPrimitive.Portal>
) );
} }
function ContextMenuItem({ function ContextMenuItem({
@ -117,8 +117,8 @@ function ContextMenuItem({
variant = "default", variant = "default",
...props ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & { }: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean inset?: boolean;
variant?: "default" | "destructive" variant?: "default" | "destructive";
}) { }) {
return ( return (
<ContextMenuPrimitive.Item <ContextMenuPrimitive.Item
@ -126,12 +126,12 @@ function ContextMenuItem({
data-inset={inset} data-inset={inset}
data-variant={variant} data-variant={variant}
className={cn( className={cn(
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!", "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive! 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} {...props}
/> />
) );
} }
function ContextMenuCheckboxItem({ function ContextMenuCheckboxItem({
@ -144,8 +144,8 @@ function ContextMenuCheckboxItem({
<ContextMenuPrimitive.CheckboxItem <ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item" data-slot="context-menu-checkbox-item"
className={cn( className={cn(
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "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} checked={checked}
{...props} {...props}
@ -157,7 +157,7 @@ function ContextMenuCheckboxItem({
</span> </span>
{children} {children}
</ContextMenuPrimitive.CheckboxItem> </ContextMenuPrimitive.CheckboxItem>
) );
} }
function ContextMenuRadioItem({ function ContextMenuRadioItem({
@ -169,8 +169,8 @@ function ContextMenuRadioItem({
<ContextMenuPrimitive.RadioItem <ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item" data-slot="context-menu-radio-item"
className={cn( className={cn(
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "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} {...props}
> >
@ -181,7 +181,7 @@ function ContextMenuRadioItem({
</span> </span>
{children} {children}
</ContextMenuPrimitive.RadioItem> </ContextMenuPrimitive.RadioItem>
) );
} }
function ContextMenuLabel({ function ContextMenuLabel({
@ -189,19 +189,19 @@ function ContextMenuLabel({
inset, inset,
...props ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & { }: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<ContextMenuPrimitive.Label <ContextMenuPrimitive.Label
data-slot="context-menu-label" data-slot="context-menu-label"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"px-2 py-1.5 text-sm font-medium text-foreground data-[inset]:pl-8", "text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function ContextMenuSeparator({ function ContextMenuSeparator({
@ -211,10 +211,10 @@ function ContextMenuSeparator({
return ( return (
<ContextMenuPrimitive.Separator <ContextMenuPrimitive.Separator
data-slot="context-menu-separator" data-slot="context-menu-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)} className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props} {...props}
/> />
) );
} }
function ContextMenuShortcut({ function ContextMenuShortcut({
@ -225,12 +225,12 @@ function ContextMenuShortcut({
<span <span
data-slot="context-menu-shortcut" data-slot="context-menu-shortcut"
className={cn( className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground", "text-muted-foreground ml-auto text-xs tracking-widest",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { export {
@ -249,4 +249,4 @@ export {
ContextMenuSubContent, ContextMenuSubContent,
ContextMenuSubTrigger, ContextMenuSubTrigger,
ContextMenuRadioGroup, ContextMenuRadioGroup,
} };

View File

@ -82,7 +82,7 @@ export function DropdownSelector<T extends string>({
> >
<span className="flex w-full items-center justify-center gap-1"> <span className="flex w-full items-center justify-center gap-1">
{/* {truncateMiddle(selectedOption?.label ?? value, 20)} */} {/* {truncateMiddle(selectedOption?.label ?? value, 20)} */}
{truncateMiddle("hfiqwertyuiopasdfghjklxcvbnm.html", 20)} {truncateMiddle("hfiqwertyuiopasdfghjklxcvbnm.html", 20)}
{isOpen ? <ChevronUpIcon /> : <ChevronDownIcon />} {isOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}
</span> </span>
@ -101,8 +101,7 @@ export function DropdownSelector<T extends string>({
title={option.label} title={option.label}
> >
{/* {truncateMiddle(option.label,50)} */} {/* {truncateMiddle(option.label,50)} */}
{truncateMiddle("hfiqwertyuiopasdfghjklxcvbnm.html",20)} {truncateMiddle("hfiqwertyuiopasdfghjklxcvbnm.html", 20)}
</DropdownMenuRadioItem> </DropdownMenuRadioItem>
))} ))}
</DropdownMenuRadioGroup> </DropdownMenuRadioGroup>

View File

@ -1,9 +1,9 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider" import * as SliderPrimitive from "@radix-ui/react-slider";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Slider({ function Slider({
className, className,
@ -20,8 +20,8 @@ function Slider({
: Array.isArray(defaultValue) : Array.isArray(defaultValue)
? defaultValue ? defaultValue
: [min, max], : [min, max],
[value, defaultValue, min, max] [value, defaultValue, min, max],
) );
return ( return (
<SliderPrimitive.Root <SliderPrimitive.Root
@ -32,20 +32,20 @@ function Slider({
max={max} max={max}
className={cn( className={cn(
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col", "relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
className className,
)} )}
{...props} {...props}
> >
<SliderPrimitive.Track <SliderPrimitive.Track
data-slot="slider-track" data-slot="slider-track"
className={cn( className={cn(
"relative grow overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5" "bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5",
)} )}
> >
<SliderPrimitive.Range <SliderPrimitive.Range
data-slot="slider-range" data-slot="slider-range"
className={cn( className={cn(
"absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full" "bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
)} )}
/> />
</SliderPrimitive.Track> </SliderPrimitive.Track>
@ -53,11 +53,11 @@ function Slider({
<SliderPrimitive.Thumb <SliderPrimitive.Thumb
data-slot="slider-thumb" data-slot="slider-thumb"
key={index} key={index}
className="block size-4 shrink-0 rounded-full border border-primary bg-white shadow-sm ring-ring/50 transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50" className="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
/> />
))} ))}
</SliderPrimitive.Root> </SliderPrimitive.Root>
) );
} }
export { Slider } export { Slider };

View File

@ -1,3 +1,4 @@
import ExcelJS from "exceljs";
import JSZip from "jszip"; import JSZip from "jszip";
import { import {
DownloadIcon, DownloadIcon,
@ -17,7 +18,6 @@ import {
} from "react"; } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Streamdown } from "streamdown"; import { Streamdown } from "streamdown";
import ExcelJS from "exceljs";
import { import {
Artifact, Artifact,
@ -73,13 +73,11 @@ let revoGridLoaderPromise: Promise<void> | null = null;
function ensureRevoGridDefined() { function ensureRevoGridDefined() {
if (typeof window === "undefined") return Promise.resolve(); if (typeof window === "undefined") return Promise.resolve();
if (window.customElements.get("revo-grid")) return Promise.resolve(); if (window.customElements.get("revo-grid")) return Promise.resolve();
if (!revoGridLoaderPromise) { revoGridLoaderPromise ??= import("@revolist/revogrid/loader").then(
revoGridLoaderPromise = import("@revolist/revogrid/loader").then( ({ defineCustomElements }) => {
({ defineCustomElements }) => { defineCustomElements(window);
defineCustomElements(window); },
}, );
);
}
return revoGridLoaderPromise; return revoGridLoaderPromise;
} }
@ -99,18 +97,45 @@ function toGridCellText(cell: ExcelJS.Cell): string {
const value = cell.value; const value = cell.value;
if (value == null) return ""; if (value == null) return "";
if (value instanceof Date) return value.toISOString(); if (value instanceof Date) return value.toISOString();
if (
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean" ||
typeof value === "bigint"
) {
return String(value);
}
if (typeof value === "object") { if (typeof value === "object") {
if ("result" in value && value.result != null) { if ("result" in value && value.result != null) {
return String(value.result); const result = value.result;
if (
typeof result === "string" ||
typeof result === "number" ||
typeof result === "boolean" ||
typeof result === "bigint"
) {
return String(result);
}
} }
if ("text" in value && value.text) { if ("text" in value && value.text) {
return String(value.text); const text = value.text;
if (
typeof text === "string" ||
typeof text === "number" ||
typeof text === "boolean" ||
typeof text === "bigint"
) {
return String(text);
}
} }
if ("hyperlink" in value && value.hyperlink) { if ("hyperlink" in value && value.hyperlink) {
return String(value.hyperlink); const hyperlink = value.hyperlink;
if (typeof hyperlink === "string") {
return hyperlink;
}
} }
} }
return String(value); return "";
} }
function toRevoGridSheetData(worksheet: ExcelJS.Worksheet): RevoGridSheetData { function toRevoGridSheetData(worksheet: ExcelJS.Worksheet): RevoGridSheetData {
@ -469,7 +494,6 @@ export function ArtifactFileDetail({
{isWriteFile ? ( {isWriteFile ? (
<div className="w-full overflow-hidden px-2 text-center text-ellipsis whitespace-nowrap"> <div className="w-full overflow-hidden px-2 text-center text-ellipsis whitespace-nowrap">
{truncateMiddle(getFileName(filepath), 20)} {truncateMiddle(getFileName(filepath), 20)}
</div> </div>
) : ( ) : (
<DropdownSelector <DropdownSelector
@ -1703,12 +1727,18 @@ export const ArtifactZoomSelector = ({
</svg> </svg>
</button> </button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" sideOffset={8} className="w-52 p-[20px] "> <DropdownMenuContent
align="start"
sideOffset={8}
className="w-52 p-[20px]"
>
<div className="mb-2 flex items-center justify-between"> <div className="mb-2 flex items-center justify-between">
<span className="text-muted-foreground text-xs"> <span className="text-muted-foreground text-xs">
{ZOOM_LEVELS[0]}% {ZOOM_LEVELS[0]}%
</span> </span>
<span className="text-foreground text-xs font-medium">{value}%</span> <span className="text-foreground text-xs font-medium">
{value}%
</span>
</div> </div>
<Slider <Slider
min={0} min={0}

View File

@ -160,7 +160,9 @@ const ChatBox: React.FC<{
) : ( ) : (
<div className="flex size-full max-w-(--container-width-sm) flex-col justify-center p-4 pt-8"> <div className="flex size-full max-w-(--container-width-sm) flex-col justify-center p-4 pt-8">
<header className="shrink-0"> <header className="shrink-0">
<h2 className="text-lg font-medium">{t.common.artifacts}</h2> <h2 className="text-lg font-medium">
{t.common.artifacts}
</h2>
</header> </header>
<main className="min-h-0 grow"> <main className="min-h-0 grow">
<ArtifactFileList <ArtifactFileList

View File

@ -1,7 +1,5 @@
"use client"; "use client";
import { useRouter } from "next/navigation";
import type { ChatStatus } from "ai"; import type { ChatStatus } from "ai";
import { import {
CheckIcon, CheckIcon,
@ -15,6 +13,8 @@ import {
XIcon, XIcon,
ZapIcon, ZapIcon,
} from "lucide-react"; } from "lucide-react";
import type { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
import { useRouter } from "next/navigation";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { import {
useCallback, useCallback,
@ -26,6 +26,7 @@ import {
type KeyboardEvent, type KeyboardEvent,
type ComponentProps, type ComponentProps,
} from "react"; } from "react";
import { toast } from "sonner";
import { import {
PromptInput, PromptInput,
@ -71,15 +72,14 @@ import { useI18n } from "@/core/i18n/hooks";
import type { SelectedSkillPayloadItem } from "@/core/i18n/locales/types"; import type { SelectedSkillPayloadItem } from "@/core/i18n/locales/types";
import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages"; import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
import { useModels } from "@/core/models/hooks"; import { useModels } from "@/core/models/hooks";
import type { AgentThreadContext } from "@/core/threads";
import { import {
MENTION_REFERENCE_EVENT, MENTION_REFERENCE_EVENT,
type MentionReferenceEventDetail, type MentionReferenceEventDetail,
} from "@/core/threads/reference-events"; } from "@/core/threads/reference-events";
import type { AgentThreadContext } from "@/core/threads";
import { useUploadedFiles } from "@/core/uploads/hooks"; import { useUploadedFiles } from "@/core/uploads/hooks";
import { useIframeSkill } from "@/hooks/use-iframe-skill"; import { useIframeSkill } from "@/hooks/use-iframe-skill";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { toast } from "sonner";
import { import {
ModelSelector, ModelSelector,
@ -91,12 +91,12 @@ import {
ModelSelectorTrigger, ModelSelectorTrigger,
} from "../ai-elements/model-selector"; } from "../ai-elements/model-selector";
import { Suggestion, Suggestions } from "../ai-elements/suggestion"; import { Suggestion, Suggestions } from "../ai-elements/suggestion";
import { ScrollArea } from "../ui/scroll-area";
import { useThread } from "./messages/context";
import { ModeHoverGuide } from "./mode-hover-guide"; import { ModeHoverGuide } from "./mode-hover-guide";
import { Tooltip } from "./tooltip"; import { Tooltip } from "./tooltip";
import { useThread } from "./messages/context";
import type { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
import { ScrollArea } from "../ui/scroll-area";
const MAX_REFERENCES_PER_MESSAGE = 10; const MAX_REFERENCES_PER_MESSAGE = 10;
@ -373,7 +373,14 @@ export function InputBox({
}); });
setReferences([]); setReferences([]);
}, },
[showWelcomeStyle, onSubmit, onStop, references, status, iframeSkill.selectedSkills], [
showWelcomeStyle,
onSubmit,
onStop,
references,
status,
iframeSkill.selectedSkills,
],
); );
const requestFormSubmit = useCallback(() => { const requestFormSubmit = useCallback(() => {
@ -381,24 +388,27 @@ export function InputBox({
form?.requestSubmit(); form?.requestSubmit();
}, []); }, []);
const addMentionReference = useCallback((reference: PromptInputReference) => { const addMentionReference = useCallback(
setReferences((prev) => { (reference: PromptInputReference) => {
const exists = prev.some( setReferences((prev) => {
(item) => const exists = prev.some(
item.ref_source === reference.ref_source && (item) =>
item.path === reference.path && item.ref_source === reference.ref_source &&
item.filename === reference.filename, item.path === reference.path &&
); item.filename === reference.filename,
if (exists) { );
return prev; if (exists) {
} return prev;
if (prev.length >= MAX_REFERENCES_PER_MESSAGE) { }
toast.error(t.inputBox.maxReferencesReached); if (prev.length >= MAX_REFERENCES_PER_MESSAGE) {
return prev; toast.error(t.inputBox.maxReferencesReached);
} return prev;
return prev.concat(reference); }
}); return prev.concat(reference);
}, [t.inputBox.maxReferencesReached]); });
},
[t.inputBox.maxReferencesReached],
);
const selectMentionCandidate = useCallback( const selectMentionCandidate = useCallback(
(candidate: MentionCandidate) => { (candidate: MentionCandidate) => {
@ -433,7 +443,7 @@ export function InputBox({
useEffect(() => { useEffect(() => {
const onMentionReference = (event: Event) => { const onMentionReference = (event: Event) => {
const detail = (event as CustomEvent<MentionReferenceEventDetail>).detail; const detail = (event as CustomEvent<MentionReferenceEventDetail>).detail;
if (!detail || detail.threadId !== threadIdFromProps) { if (detail?.threadId !== threadIdFromProps) {
return; return;
} }
addMentionReference({ addMentionReference({
@ -493,14 +503,15 @@ export function InputBox({
} }
if (event.key === "ArrowDown") { if (event.key === "ArrowDown") {
event.preventDefault(); event.preventDefault();
setActiveMentionIndex((prev) => setActiveMentionIndex(
(prev + 1) % filteredMentionCandidates.length, (prev) => (prev + 1) % filteredMentionCandidates.length,
); );
} else if (event.key === "ArrowUp") { } else if (event.key === "ArrowUp") {
event.preventDefault(); event.preventDefault();
setActiveMentionIndex((prev) => setActiveMentionIndex(
(prev - 1 + filteredMentionCandidates.length) % (prev) =>
filteredMentionCandidates.length, (prev - 1 + filteredMentionCandidates.length) %
filteredMentionCandidates.length,
); );
} else if (event.key === "Enter") { } else if (event.key === "Enter") {
event.preventDefault(); event.preventDefault();
@ -693,7 +704,7 @@ export function InputBox({
align="start" align="start"
side="top" side="top"
sideOffset={8} sideOffset={8}
className="w-[min(32rem,var(--radix-dropdown-menu-trigger-width)+28rem)] max-h-[400px] overflow-y-hidden p-[20px]" className="max-h-[400px] w-[min(32rem,var(--radix-dropdown-menu-trigger-width)+28rem)] overflow-y-hidden p-[20px]"
data-testid="mention-candidate-panel" data-testid="mention-candidate-panel"
onCloseAutoFocus={(event) => { onCloseAutoFocus={(event) => {
event.preventDefault(); event.preventDefault();
@ -704,52 +715,54 @@ export function InputBox({
{t.inputBox.addReference} {t.inputBox.addReference}
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator className="mx-0 mt-[20px] mb-0" /> <DropdownMenuSeparator className="mx-0 mt-[20px] mb-0" />
<DropdownMenuGroup className="flex pt-[20px] px-0 max-h-[480px] flex-col gap-[10px]"> <DropdownMenuGroup className="flex max-h-[480px] flex-col gap-[10px] px-0 pt-[20px]">
<ScrollArea className="h-[480px]" data-state="hidden"> <ScrollArea className="h-[480px]" data-state="hidden">
{filteredMentionCandidates.map((candidate, index) => { {filteredMentionCandidates.map((candidate, index) => {
const detail = [candidate.typeLabel, candidate.pathTail] const detail = [candidate.typeLabel, candidate.pathTail]
.filter(Boolean) .filter(Boolean)
.join(" · "); .join(" · ");
return ( return (
<DropdownMenuItem <DropdownMenuItem
key={candidate.key} key={candidate.key}
className={cn( className={cn(
"flex items-center justify-between gap-3 rounded-md px-2 py-2 text-left", "flex items-center justify-between gap-3 rounded-md px-2 py-2 text-left",
index === activeMentionIndex && "bg-accent", index === activeMentionIndex && "bg-accent",
)} )}
data-active={index === activeMentionIndex ? "true" : "false"} data-active={
data-candidate-key={candidate.key} index === activeMentionIndex ? "true" : "false"
data-testid="mention-candidate-item" }
aria-label={`${candidate.filename} ${candidate.typeLabel}${candidate.pathTail ? ` ${candidate.pathTail}` : ""}`} data-candidate-key={candidate.key}
onFocus={() => setActiveMentionIndex(index)} data-testid="mention-candidate-item"
onMouseDown={(event) => event.preventDefault()} aria-label={`${candidate.filename} ${candidate.typeLabel}${candidate.pathTail ? ` ${candidate.pathTail}` : ""}`}
onSelect={(event) => { onFocus={() => setActiveMentionIndex(index)}
event.preventDefault(); onMouseDown={(event) => event.preventDefault()}
selectMentionCandidate(candidate); onSelect={(event) => {
}} event.preventDefault();
> selectMentionCandidate(candidate);
{candidate.isImage && candidate.previewUrl ? ( }}
<img >
src={candidate.previewUrl} {candidate.isImage && candidate.previewUrl ? (
alt={candidate.filename} <img
className="h-10 w-10 shrink-0 rounded-md border object-cover object-top" src={candidate.previewUrl}
/> alt={candidate.filename}
) : ( className="h-10 w-10 shrink-0 rounded-md border object-cover object-top"
<div className="bg-muted text-muted-foreground flex h-10 w-10 shrink-0 items-center justify-center rounded-md border text-[10px] font-semibold"> />
{fileExtensionLabel(candidate.filename)} ) : (
</div> <div className="bg-muted text-muted-foreground flex h-10 w-10 shrink-0 items-center justify-center rounded-md border text-[10px] font-semibold">
)} {fileExtensionLabel(candidate.filename)}
<div className="min-w-0 flex-1"> </div>
<span className="block truncate text-sm font-medium"> )}
{candidate.filename} <div className="min-w-0 flex-1">
</span> <span className="block truncate text-sm font-medium">
<span className="text-muted-foreground block truncate text-xs"> {candidate.filename}
{detail} </span>
</span> <span className="text-muted-foreground block truncate text-xs">
</div> {detail}
</DropdownMenuItem> </span>
); </div>
})} </DropdownMenuItem>
);
})}
</ScrollArea> </ScrollArea>
</DropdownMenuGroup> </DropdownMenuGroup>
</DropdownMenuContent> </DropdownMenuContent>
@ -768,7 +781,7 @@ export function InputBox({
className={cn( className={cn(
"flex transition-all duration-300 ease-out", "flex transition-all duration-300 ease-out",
!effectiveIsFocused && !effectiveIsFocused &&
"pointer-events-none invisible h-[0px] translate-y-2 p-[0px] opacity-0", "pointer-events-none invisible h-[0px] translate-y-2 p-[0px] opacity-0",
)} )}
> >
<PromptInputTools className="min-w-0 flex-1 gap-[20px]"> <PromptInputTools className="min-w-0 flex-1 gap-[20px]">
@ -781,11 +794,13 @@ export function InputBox({
/> />
</PromptInputActionMenuContent> </PromptInputActionMenuContent>
</PromptInputActionMenu> */} </PromptInputActionMenu> */}
{showWelcomeStyle && <HistoryButton {showWelcomeStyle && (
className="px-2!" <HistoryButton
router={router} className="px-2!"
threadId={threadIdFromProps} router={router}
/>} threadId={threadIdFromProps}
/>
)}
<AddAttachmentsButton className="px-2!" /> <AddAttachmentsButton className="px-2!" />
<IframeSkillDialogButton <IframeSkillDialogButton
className="px-2!" className="px-2!"
@ -1096,7 +1111,6 @@ function HistoryButton({
strokeLinejoin="round" strokeLinejoin="round"
/> />
</svg> </svg>
</PromptInputButton> </PromptInputButton>
</Tooltip> </Tooltip>
); );
@ -1205,7 +1219,10 @@ function AttachmentPreviewBar({
</PromptInputAttachments> </PromptInputAttachments>
)} )}
{hasReferences && ( {hasReferences && (
<div className="inline-flex flex-row flex-wrap items-center gap-2 rounded-xl p-2" data-testid="reference-inline-preview"> <div
className="inline-flex flex-row flex-wrap items-center gap-2 rounded-xl p-2"
data-testid="reference-inline-preview"
>
{references.map((reference) => { {references.map((reference) => {
const referenceUrl = const referenceUrl =
threadId && reference.path threadId && reference.path
@ -1215,7 +1232,7 @@ function AttachmentPreviewBar({
}) })
: null; : null;
const filename = reference.filename ?? "reference"; const filename = reference.filename ?? "reference";
const imageMatch = filename.match(/\.(png|jpe?g|gif|webp|bmp|svg)$/i); const imageMatch = /\.(png|jpe?g|gif|webp|bmp|svg)$/i.exec(filename);
const extension = imageMatch?.[1]?.toLowerCase(); const extension = imageMatch?.[1]?.toLowerCase();
const mediaType = extension const mediaType = extension
? extension === "jpg" ? extension === "jpg"

View File

@ -41,8 +41,8 @@ function parseTableData(table: HTMLTableElement): TableData {
(cell.textContent ?? "").trim(), (cell.textContent ?? "").trim(),
); );
const rows = Array.from(table.querySelectorAll("tbody tr")).map((row) => const rows = Array.from(table.querySelectorAll("tbody tr")).map((row) =>
Array.from(row.querySelectorAll("td")).map( Array.from(row.querySelectorAll("td")).map((cell) =>
(cell) => (cell.textContent ?? "").trim(), (cell.textContent ?? "").trim(),
), ),
); );
return { headers, rows }; return { headers, rows };
@ -70,7 +70,9 @@ function MarkdownTable({
const handleCopy = useCallback( const handleCopy = useCallback(
async (event: MouseEvent<HTMLButtonElement>) => { async (event: MouseEvent<HTMLButtonElement>) => {
const wrapper = event.currentTarget.closest('[data-streamdown="table-wrapper"]'); const wrapper = event.currentTarget.closest(
'[data-streamdown="table-wrapper"]',
);
const table = wrapper?.querySelector("table"); const table = wrapper?.querySelector("table");
if (!(table instanceof HTMLTableElement)) return; if (!(table instanceof HTMLTableElement)) return;
@ -89,10 +91,13 @@ function MarkdownTable({
); );
return ( return (
<div className="my-4 flex flex-col space-y-2" data-streamdown="table-wrapper"> <div
className="my-4 flex flex-col space-y-2"
data-streamdown="table-wrapper"
>
<div className="flex items-center justify-end gap-1"> <div className="flex items-center justify-end gap-1">
<button <button
className="cursor-pointer p-1 text-muted-foreground transition-all hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50" className="text-muted-foreground hover:text-foreground cursor-pointer p-1 transition-all disabled:cursor-not-allowed disabled:opacity-50"
disabled={isLoading} disabled={isLoading}
onClick={handleCopy} onClick={handleCopy}
title={copyLabel} title={copyLabel}
@ -103,7 +108,10 @@ function MarkdownTable({
</div> </div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table <table
className={cn("w-full border-collapse border border-border", className)} className={cn(
"border-border w-full border-collapse border",
className,
)}
data-streamdown="table" data-streamdown="table"
{...props} {...props}
> >

View File

@ -40,7 +40,6 @@ import { Tooltip } from "../tooltip";
import { MarkdownContent } from "./markdown-content"; import { MarkdownContent } from "./markdown-content";
export function MessageGroup({ export function MessageGroup({
className, className,
messages, messages,
@ -87,11 +86,7 @@ export function MessageGroup({
const rehypePlugins = useRehypeSplitWordsIntoSpans(false); const rehypePlugins = useRehypeSplitWordsIntoSpans(false);
const thinkingComponents = useMemo( const thinkingComponents = useMemo(
() => ({ () => ({
code: ({ code: ({ className, children, ...props }: ComponentProps<"code">) => {
className,
children,
...props
}: ComponentProps<"code">) => {
const isBlock = const isBlock =
typeof className === "string" && className.includes("language-"); typeof className === "string" && className.includes("language-");
if (!isBlock) { if (!isBlock) {
@ -126,7 +121,7 @@ export function MessageGroup({
<Button <Button
key="above" key="above"
// 等宋 // 等宋
className="w-full items-start justify-start text-left h-auto! py-4" className="h-auto! w-full items-start justify-start py-4 text-left"
variant="ghost" variant="ghost"
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();

View File

@ -34,10 +34,10 @@ import {
stripUploadedFilesTag, stripUploadedFilesTag,
type FileInMessage, type FileInMessage,
} from "@/core/messages/utils"; } from "@/core/messages/utils";
import { dispatchMentionReference } from "@/core/threads/reference-events";
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype"; import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
import { materializeSkillYaml } from "@/core/skills"; import { materializeSkillYaml } from "@/core/skills";
import { humanMessagePlugins } from "@/core/streamdown"; import { humanMessagePlugins } from "@/core/streamdown";
import { dispatchMentionReference } from "@/core/threads/reference-events";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { CopyButton } from "../copy-button"; import { CopyButton } from "../copy-button";
@ -424,22 +424,22 @@ function RichFileCard({
/> />
</a> </a>
</ContextMenuTrigger> </ContextMenuTrigger>
<ContextMenuContent className="min-w-[120px]"> <ContextMenuContent className="min-w-[120px]">
<ContextMenuItem <ContextMenuItem
disabled={!canReference} disabled={!canReference}
onClick={() => { onClick={() => {
if (!file.path) return; if (!file.path) return;
dispatchMentionReference({ dispatchMentionReference({
threadId, threadId,
filename: file.filename, filename: file.filename,
path: file.path, path: file.path,
ref_source: refSource, ref_source: refSource,
}); });
}} }}
> >
{t.common.reference} {t.common.reference}
</ContextMenuItem> </ContextMenuItem>
</ContextMenuContent> </ContextMenuContent>
</ContextMenu> </ContextMenu>
); );
} }

View File

@ -261,7 +261,8 @@ export const zhCN: Translations = {
noArtifactSelectedTitle: "未选择生成文件", noArtifactSelectedTitle: "未选择生成文件",
noArtifactSelectedDescription: "请选择一个生成文件以查看详情", noArtifactSelectedDescription: "请选择一个生成文件以查看详情",
exitDialogTitle: "提示", exitDialogTitle: "提示",
exitDialogDescription: "历史记录每七天自动删除,现在将返回欢迎页,是否继续?", exitDialogDescription:
"历史记录每七天自动删除,现在将返回欢迎页,是否继续?",
exitDialogConfirm: "确定", exitDialogConfirm: "确定",
selectedSkillLoadFailed: "技能加载失败", selectedSkillLoadFailed: "技能加载失败",
unknownErrorRetry: "发生了未知错误,请稍后重试。", unknownErrorRetry: "发生了未知错误,请稍后重试。",

View File

@ -4,15 +4,15 @@ import type { Model } from "./types";
export async function loadModels() { export async function loadModels() {
const res = await fetch(`${getBackendBaseURL()}/api/models`); const res = await fetch(`${getBackendBaseURL()}/api/models`);
if (res.status >= 500 && res.status < 600) { if (res.status >= 500 && res.status < 600) {
throw new Error(`Server error: ${res.status}`); throw new Error(`Server error: ${res.status}`);
} }
if (!res.ok) { if (!res.ok) {
throw new Error(`HTTP error: ${res.status}`); throw new Error(`HTTP error: ${res.status}`);
} }
const { models } = (await res.json()) as { models: Model[] }; const { models } = (await res.json()) as { models: Model[] };
return models; return models;
} }

View File

@ -17,8 +17,8 @@ import type { UploadedFileInfo } from "../uploads";
import { listUploadedFiles, uploadFiles } from "../uploads"; import { listUploadedFiles, uploadFiles } from "../uploads";
import type { UploadTarget } from "../uploads/api"; import type { UploadTarget } from "../uploads/api";
import { buildFilesForSubmit } from "./submit-files";
import { buildPriorityHintText, composeSubmitText } from "./priority-hint"; import { buildPriorityHintText, composeSubmitText } from "./priority-hint";
import { buildFilesForSubmit } from "./submit-files";
import type { import type {
AgentThread, AgentThread,
AgentThreadContext, AgentThreadContext,
@ -268,8 +268,7 @@ export function useThreadStream({
const now = Date.now(); const now = Date.now();
const lastToast = lastErrorToastRef.current; const lastToast = lastErrorToastRef.current;
if ( if (
lastToast && lastToast?.message === message &&
lastToast.message === message &&
now - lastToast.timestamp < STREAM_ERROR_TOAST_DEDUPE_WINDOW_MS now - lastToast.timestamp < STREAM_ERROR_TOAST_DEDUPE_WINDOW_MS
) { ) {
return; return;

View File

@ -2,6 +2,7 @@ import { useRouter, useSearchParams } from "next/navigation";
import { useState, useEffect, useCallback, useRef } from "react"; import { useState, useEffect, useCallback, useRef } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { useI18n } from "@/core/i18n/hooks";
import { import {
POST_MESSAGE_TYPES, POST_MESSAGE_TYPES,
RECEIVE_MESSAGE_TYPES, RECEIVE_MESSAGE_TYPES,
@ -10,7 +11,6 @@ import {
type SelectedSkillPayloadItem, type SelectedSkillPayloadItem,
sendToParent, sendToParent,
} from "@/core/iframe-messages"; } from "@/core/iframe-messages";
import { useI18n } from "@/core/i18n/hooks";
import { bootstrapRemoteSkill } from "@/core/skills/api"; import { bootstrapRemoteSkill } from "@/core/skills/api";
// Skill 数据类型 // Skill 数据类型
@ -39,8 +39,20 @@ function parseStoredSkills(raw: string | null): SkillData[] {
.map((item) => { .map((item) => {
if (typeof item !== "object" || item === null) return null; if (typeof item !== "object" || item === null) return null;
const record = item as Record<string, unknown>; const record = item as Record<string, unknown>;
const skillId = String(record.skill_id ?? "").trim(); const rawSkillId = record.skill_id;
const title = String(record.title ?? "").trim(); const skillId =
typeof rawSkillId === "string"
? rawSkillId.trim()
: typeof rawSkillId === "number"
? String(rawSkillId)
: "";
const rawTitle = record.title;
const title =
typeof rawTitle === "string"
? rawTitle.trim()
: typeof rawTitle === "number"
? String(rawTitle)
: "";
if (!skillId || !title) return null; if (!skillId || !title) return null;
return { skill_id: skillId, title }; return { skill_id: skillId, title };
}) })
@ -84,7 +96,11 @@ export function useIframeSkill(
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const threadIdFromQuery = searchParams.get("thread_id"); const threadIdFromQuery = searchParams.get("thread_id");
const threadId = options?.threadId?.trim() || threadIdFromQuery; const threadIdFromOptions = options?.threadId?.trim();
const threadId =
threadIdFromOptions && threadIdFromOptions.length > 0
? threadIdFromOptions
: threadIdFromQuery;
const isChattingFromQuery = searchParams.get("is_chatting"); const isChattingFromQuery = searchParams.get("is_chatting");
const lastThreadIdRef = useRef<string | null>(null); const lastThreadIdRef = useRef<string | null>(null);
@ -316,7 +332,8 @@ export function useIframeSkill(
setSelectedSkills(normalizedSkills); setSelectedSkills(normalizedSkills);
toast.success(t.skills.loadSuccessWithTitle(title), { toast.success(t.skills.loadSuccessWithTitle(title), {
description: result.message || t.skills.createdFiles(result.created_files), description:
result.message || t.skills.createdFiles(result.created_files),
}); });
return true; return true;
@ -325,7 +342,9 @@ export function useIframeSkill(
removeFailedSkills(failedIds); removeFailedSkills(failedIds);
toast.dismiss("suggest-skill-bootstrap"); toast.dismiss("suggest-skill-bootstrap");
const message = const message =
error instanceof Error ? error.message : t.skills.networkRequestFailed; error instanceof Error
? error.message
: t.skills.networkRequestFailed;
toast.error(t.skills.loadFailedWithTitle(title), { toast.error(t.skills.loadFailedWithTitle(title), {
description: message, description: message,
}); });

View File

@ -2,12 +2,12 @@ import { useSearchParams } from "next/navigation";
import { useEffect, useCallback, useState, useRef } from "react"; import { useEffect, useCallback, useState, useRef } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { useI18n } from "@/core/i18n/hooks";
import { import {
isSelectedSkillMessage, isSelectedSkillMessage,
isSelectedSkillsMessage, isSelectedSkillsMessage,
type SelectedSkillPayloadItem, type SelectedSkillPayloadItem,
} from "@/core/iframe-messages"; } from "@/core/iframe-messages";
import { useI18n } from "@/core/i18n/hooks";
import { bootstrapRemoteSkill } from "@/core/skills/api"; import { bootstrapRemoteSkill } from "@/core/skills/api";
/** 技能基础数据 */ /** 技能基础数据 */
@ -105,7 +105,8 @@ export function useSelectedSkillListener({
if (result.success) { if (result.success) {
skillBootstrappedKeyRef.current = initKey; skillBootstrappedKeyRef.current = initKey;
toast.success(t.skills.loadSuccessWithTitle(title), { toast.success(t.skills.loadSuccessWithTitle(title), {
description: result.message || t.skills.createdFiles(result.created_files), description:
result.message || t.skills.createdFiles(result.created_files),
duration: 4000, duration: 4000,
}); });
} else { } else {

View File

@ -1,5 +1,6 @@
import { clsx, type ClassValue } from "clsx"; import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages"; import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {

View File

@ -77,27 +77,32 @@
"Segoe UI Symbol", "Noto Color Emoji"; "Segoe UI Symbol", "Noto Color Emoji";
--animate-fade-in: fade-in 1.1s; --animate-fade-in: fade-in 1.1s;
@keyframes fade-in { @keyframes fade-in {
0% { 0% {
opacity: 0; opacity: 0;
} }
100% { 100% {
opacity: 1; opacity: 1;
} }
} }
--animate-fade-in-up: fade-in-up 0.15s ease-in-out forwards; --animate-fade-in-up: fade-in-up 0.15s ease-in-out forwards;
@keyframes fade-in-up { @keyframes fade-in-up {
0% { 0% {
opacity: 0; opacity: 0;
transform: translateY(1rem) scale(1.2); transform: translateY(1rem) scale(1.2);
} }
100% { 100% {
opacity: 1; opacity: 1;
} }
} }
--animate-bouncing: bouncing 0.5s infinite alternate; --animate-bouncing: bouncing 0.5s infinite alternate;
@keyframes bouncing { @keyframes bouncing {
to { to {
opacity: 0.1; opacity: 0.1;
@ -106,11 +111,13 @@
} }
--animate-skeleton-entrance: skeleton-entrance 0.35s ease-out forwards; --animate-skeleton-entrance: skeleton-entrance 0.35s ease-out forwards;
@keyframes skeleton-entrance { @keyframes skeleton-entrance {
0% { 0% {
opacity: 0; opacity: 0;
transform: scaleX(0); transform: scaleX(0);
} }
100% { 100% {
opacity: 1; opacity: 1;
transform: scaleX(1); transform: scaleX(1);
@ -118,11 +125,13 @@
} }
--animate-suggestion-in: suggestion-in 0.2s ease-out forwards; --animate-suggestion-in: suggestion-in 0.2s ease-out forwards;
@keyframes suggestion-in { @keyframes suggestion-in {
0% { 0% {
opacity: 0; opacity: 0;
transform: translateY(-1.25rem); transform: translateY(-1.25rem);
} }
100% { 100% {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
@ -130,17 +139,21 @@
} }
--animate-wave: wave 0.6s ease-in-out 2; --animate-wave: wave 0.6s ease-in-out 2;
@keyframes wave { @keyframes wave {
0%, 0%,
100% { 100% {
transform: rotate(0deg); transform: rotate(0deg);
} }
25% { 25% {
transform: rotate(20deg); transform: rotate(20deg);
} }
50% { 50% {
transform: rotate(0deg); transform: rotate(0deg);
} }
75% { 75% {
transform: rotate(20deg); transform: rotate(20deg);
} }
@ -188,36 +201,45 @@
--color-sidebar-ring: var(--sidebar-ring); --color-sidebar-ring: var(--sidebar-ring);
--color-tooltip-background: var(--tooltip-background); --color-tooltip-background: var(--tooltip-background);
--animate-aurora: aurora 8s ease-in-out infinite alternate; --animate-aurora: aurora 8s ease-in-out infinite alternate;
@keyframes aurora { @keyframes aurora {
0% { 0% {
background-position: 0% 50%; background-position: 0% 50%;
transform: rotate(-5deg) scale(0.9); transform: rotate(-5deg) scale(0.9);
} }
25% { 25% {
background-position: 50% 100%; background-position: 50% 100%;
transform: rotate(5deg) scale(1.1); transform: rotate(5deg) scale(1.1);
} }
50% { 50% {
background-position: 100% 50%; background-position: 100% 50%;
transform: rotate(-3deg) scale(0.95); transform: rotate(-3deg) scale(0.95);
} }
75% { 75% {
background-position: 50% 0%; background-position: 50% 0%;
transform: rotate(3deg) scale(1.05); transform: rotate(3deg) scale(1.05);
} }
100% { 100% {
background-position: 0% 50%; background-position: 0% 50%;
transform: rotate(-5deg) scale(0.9); transform: rotate(-5deg) scale(0.9);
} }
} }
--animate-shine: shine var(--duration) infinite linear; --animate-shine: shine var(--duration) infinite linear;
@keyframes shine { @keyframes shine {
0% { 0% {
background-position: 0% 0%; background-position: 0% 0%;
} }
50% { 50% {
background-position: 100% 100%; background-position: 100% 100%;
} }
to { to {
background-position: 0% 0%; background-position: 0% 0%;
} }
@ -308,22 +330,27 @@
* { * {
@apply border-border outline-ring/50; @apply border-border outline-ring/50;
} }
body { body {
@apply text-foreground; @apply text-foreground;
} }
.container-md { .container-md {
width: 100%; width: 100%;
@media (width >= 40rem) {
@media (width >=40rem) {
max-width: 40rem; max-width: 40rem;
} }
@media (width >= 48rem) {
@media (width >=48rem) {
max-width: 48rem; max-width: 48rem;
} }
@media (width >= 64rem) {
@media (width >=64rem) {
max-width: 64rem; max-width: 64rem;
} }
@media (width >= 80rem) {
@media (width >=80rem) {
max-width: 80rem; max-width: 80rem;
} }
} }
@ -375,9 +402,11 @@
0% { 0% {
background-position: 0 0; background-position: 0 0;
} }
50% { 50% {
background-position: 400% 0; background-position: 400% 0;
} }
100% { 100% {
background-position: 0 0; background-position: 0 0;
} }
@ -436,6 +465,7 @@ body {
p { p {
font-size: calc(14px * var(--zoom-scale)); font-size: calc(14px * var(--zoom-scale));
} }
/* 特别指定,代码块和正文一样的字体 */ /* 特别指定,代码块和正文一样的字体 */
code, code,
kbd, kbd,
@ -444,8 +474,9 @@ pre {
font-family: font-family:
"Microsoft YaHei", "微软雅黑", "PingFang SC", sans-serif !important; "Microsoft YaHei", "微软雅黑", "PingFang SC", sans-serif !important;
} }
pre{
border-radius: 5px; pre {
border-radius: 5px;
padding: 12px 16px; padding: 12px 16px;
} }
@ -463,12 +494,14 @@ pre{
/* 二三级标题 - 16px */ /* 二三级标题 - 16px */
[data-streamdown="heading-2"], [data-streamdown="heading-2"],
[data-streamdown="heading-3"],[data-streamdown="heading-4"] { [data-streamdown="heading-3"],
[data-streamdown="heading-4"] {
font-size: calc(16px * var(--zoom-scale)); font-size: calc(16px * var(--zoom-scale));
} }
/* 代码块 - 14px */ /* 代码块 - 14px */
[data-streamdown="code-block"] pre,code { [data-streamdown="code-block"] pre,
code {
font-size: calc(14px * var(--zoom-scale)); font-size: calc(14px * var(--zoom-scale));
} }
@ -483,56 +516,69 @@ pre{
[data-streamdown="table-cell"] { [data-streamdown="table-cell"] {
background-color: transparent; background-color: transparent;
font-size: calc(14px * var(--zoom-scale)); font-size: calc(14px * var(--zoom-scale));
height:calc(42px * var(--zoom-scale)) ; height: calc(42px * var(--zoom-scale));
} }
[data-streamdown="table-header"] { [data-streamdown="table-header"] {
background: #9c9b9b26; background: #9c9b9b26;
height: calc(50px * var(--zoom-scale)); height: calc(50px * var(--zoom-scale));
} }
[data-streamdown="table-header"] th { [data-streamdown="table-header"] th {
text-align: center; text-align: center;
font-size: calc(14px * var(--zoom-scale));
}
[data-slot="hover-card-trigger"] [data-slot="badge"]{
font-size: calc(14px * var(--zoom-scale)); font-size: calc(14px * var(--zoom-scale));
} }
[data-slot="hover-card-trigger"] [data-slot="badge"] {
font-size: calc(14px * var(--zoom-scale));
}
/* 表格四角圆角:由四个角单元格承担视觉圆角 */ /* 表格四角圆角:由四个角单元格承担视觉圆角 */
[data-streamdown="table-header"] tr:first-child > [data-streamdown="table-header-cell"]:first-child { [data-streamdown="table-header"]
tr:first-child
> [data-streamdown="table-header-cell"]:first-child {
border-top-left-radius: 5px; border-top-left-radius: 5px;
} }
[data-streamdown="table-header"] tr:first-child > [data-streamdown="table-header-cell"]:last-child { [data-streamdown="table-header"]
tr:first-child
> [data-streamdown="table-header-cell"]:last-child {
border-top-right-radius: 5px; border-top-right-radius: 5px;
} }
[data-streamdown="table-body"] tr:first-child td{
[data-streamdown="table-body"] tr:first-child td {
line-height: calc(14px * var(--zoom-scale));
padding-top: calc(20px * var(--zoom-scale)); padding-top: calc(20px * var(--zoom-scale));
} }
/* 行分隔线 */ /* 行分隔线 */
[data-streamdown="table-body"] tr{ /* [data-streamdown="table-body"] tr {
border-bottom: 1px solid var(--border); border-bottom: 1px solid black;
} } */
[data-streamdown="table-body"] tr:last-child > [data-streamdown="table-cell"]:first-child {
[data-streamdown="table-body"]
tr:last-child
> [data-streamdown="table-cell"]:first-child {
border-bottom-left-radius: 5px; border-bottom-left-radius: 5px;
} }
[data-streamdown="table-body"] tr:last-child > [data-streamdown="table-cell"]:last-child { [data-streamdown="table-body"]
tr:last-child
> [data-streamdown="table-cell"]:last-child {
border-bottom-right-radius: 5px; border-bottom-right-radius: 5px;
} }
[data-streamdown="table-body"] tr:last-child { [data-streamdown="table-body"] tr:last-child td {
padding-top: calc(50px * var(--zoom-scale)); line-height: calc(14px * var(--zoom-scale));
padding-bottom: calc(20px * var(--zoom-scale));
} }
[data-streamdown="table-row"] >[data-streamdown="table-cell"]{
line-height: 14px; [data-streamdown="table-row"] > [data-streamdown="table-cell"] {
vertical-align: top; line-height: calc(42px * var(--zoom-scale));
vertical-align: top;
text-align: center; text-align: center;
} }
.cm-line { .cm-line {
font-size: calc(14px * var(--zoom-scale)); font-size: calc(14px * var(--zoom-scale));
white-space: pre-wrap; white-space: pre-wrap;

View File

@ -164,7 +164,7 @@ export async function rewriteFirstReferenceAsArtifact(
return false; return false;
} }
let fiber = ((element as unknown as Record<string, unknown>)[fiberKey]) as let fiber = (element as unknown as Record<string, unknown>)[fiberKey] as
| { | {
return?: unknown; return?: unknown;
memoizedState?: unknown; memoizedState?: unknown;

View File

@ -5,7 +5,7 @@ import { newChatEntry, openChat, sendMessage } from "./support/chat-helpers";
function logProgress(message: string) { function logProgress(message: string) {
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
// eslint-disable-next-line no-console
console.log(`[DF-SEC][${timestamp}] ${message}`); console.log(`[DF-SEC][${timestamp}] ${message}`);
} }
@ -21,10 +21,7 @@ function parseForbiddenPrefixes() {
return prefixes; return prefixes;
} }
async function assertNoForbiddenPrefixOnScreen( async function assertNoForbiddenPrefixOnScreen(page: Page, prefixes: string[]) {
page: Page,
prefixes: string[],
) {
if (prefixes.length === 0) return; if (prefixes.length === 0) return;
const leaked = await page.evaluate((items) => { const leaked = await page.evaluate((items) => {
const text = document.body?.innerText ?? ""; const text = document.body?.innerText ?? "";
@ -64,9 +61,7 @@ async function waitForConditionWithLeakCheck({
const now = Date.now(); const now = Date.now();
if (now - lastLogAt >= logEveryMs) { if (now - lastLogAt >= logEveryMs) {
lastLogAt = now; lastLogAt = now;
logProgress( logProgress(`${label}… (${Math.round((now - start) / 1000)}s elapsed)`);
`${label}… (${Math.round((now - start) / 1000)}s elapsed)`,
);
} }
} }
await page.waitForTimeout(stepMs); await page.waitForTimeout(stepMs);
@ -113,7 +108,10 @@ async function waitForArtifactCards({
label, label,
condition: async () => { condition: async () => {
// Cards only render when the panel is open. Try to open opportunistically. // Cards only render when the panel is open. Try to open opportunistically.
if ((await fileList.count()) === 0 || !(await fileList.first().isVisible())) { if (
(await fileList.count()) === 0 ||
!(await fileList.first().isVisible())
) {
await openArtifactsPanelIfPossible(page); await openArtifactsPanelIfPossible(page);
} }
if ((await cards.count()) < minCount) return false; if ((await cards.count()) < minCount) return false;
@ -169,11 +167,7 @@ async function sendMessageSafely({
}); });
await textarea.evaluate((element) => { await textarea.evaluate((element) => {
const target = element as HTMLTextAreaElement; const target = element as HTMLTextAreaElement;
const setter = Object.getOwnPropertyDescriptor( target.value = "";
HTMLTextAreaElement.prototype,
"value",
)?.set;
setter?.call(target, "");
target.dispatchEvent(new InputEvent("input", { bubbles: true })); target.dispatchEvent(new InputEvent("input", { bubbles: true }));
}); });
await page.keyboard.insertText(text); await page.keyboard.insertText(text);
@ -241,7 +235,8 @@ test.describe("安全 / 思考块与敏感信息泄露", () => {
timeoutMs: 40_000, timeoutMs: 40_000,
label: "Wait for steps signal", label: "Wait for steps signal",
condition: async () => condition: async () =>
(await stepsSignal.count()) > 0 && (await stepsSignal.first().isVisible()), (await stepsSignal.count()) > 0 &&
(await stepsSignal.first().isVisible()),
}); });
// 按需求40s 内未出现思考块则中断后续检查(标记为 skip // 按需求40s 内未出现思考块则中断后续检查(标记为 skip
@ -256,9 +251,10 @@ test.describe("安全 / 思考块与敏感信息泄露", () => {
minCount: 1, minCount: 1,
label: "Wait for first artifact card", label: "Wait for first artifact card",
}); });
expect(firstArtifacts.ok, "未检测到 artifact-file-card图片可能未生成完成").toBe( expect(
true, firstArtifacts.ok,
); "未检测到 artifact-file-card图片可能未生成完成",
).toBe(true);
logProgress( logProgress(
`First artifact ready (count=${await firstArtifacts.cards.count()}).`, `First artifact ready (count=${await firstArtifacts.cards.count()}).`,
); );
@ -279,7 +275,10 @@ test.describe("安全 / 思考块与敏感信息泄露", () => {
minCount: beforeSecondCount + 1, minCount: beforeSecondCount + 1,
label: "Wait for second artifact card", label: "Wait for second artifact card",
}); });
expect(secondArtifacts.ok, "未检测到新的产物生成artifact 数量未增加)").toBe(true); expect(
secondArtifacts.ok,
"未检测到新的产物生成artifact 数量未增加)",
).toBe(true);
logProgress( logProgress(
`Second artifact ready (count=${await secondArtifacts.cards.count()}).`, `Second artifact ready (count=${await secondArtifacts.cards.count()}).`,
); );