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";
import { Ticker } from "@tombcato/smart-ticker";
import { FilesIcon, ListTodoIcon, XIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@ -22,6 +23,7 @@ import {
} from "@/components/workspace/artifacts";
import { useThreadChat } from "@/components/workspace/chats";
// import { DevTodoList } from "@/components/workspace/dev-todo-list";
import { IframeTestPanel } from "@/components/workspace/iframe-test-panel";
import { InputBox } from "@/components/workspace/input-box";
import { MessageList } from "@/components/workspace/messages";
import { ThreadContext } from "@/components/workspace/messages/context";
@ -39,8 +41,7 @@ import { textOfMessage } from "@/core/threads/utils";
import { env } from "@/env";
import { useSelectedSkillListener } from "@/hooks/use-selected-skill-listener";
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 motivationSlogans from "./motivation-slogans.json";
@ -489,9 +490,7 @@ export default function ChatPage() {
/>
) : (
<div className="relative flex size-full justify-center px-[20px]">
<div className="z-30">
</div>
<div className="z-30"></div>
{thread.values.artifacts?.length === 0 ? (
<ConversationEmptyState
icon={<FilesIcon />}
@ -500,8 +499,8 @@ export default function ChatPage() {
/>
) : (
<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 ">
<h2 className="text-[14px] h-[58px] leading-[58px] font-bold text-[#333333]">
<header className="flex shrink-0 items-center justify-between border-b">
<h2 className="h-[58px] text-[14px] leading-[58px] font-bold text-[#333333]">
<span>{t.common.artifacts}</span>
</h2>
<Button
@ -655,7 +654,9 @@ export default function ChatPage() {
<DevDialogContent>
<DevDialogHeader>
<DevDialogTitle>
{selectedSkillError?.title ?? t.chatPage.selectedSkillLoadFailed}
{" "}
{selectedSkillError?.title ??
t.chatPage.selectedSkillLoadFailed}
</DevDialogTitle>
</DevDialogHeader>
<p className="text-muted-foreground text-sm">

View File

@ -36,7 +36,7 @@ export const Message = ({
"group flex w-full flex-col gap-2",
from === "user"
? 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,
)}
{...props}

View File

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

View File

@ -1,15 +1,15 @@
"use client"
"use client";
import * as React from "react"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import * as React from "react";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function ContextMenu({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
}
function ContextMenuTrigger({
@ -17,7 +17,7 @@ function ContextMenuTrigger({
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
return (
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
)
);
}
function ContextMenuGroup({
@ -25,7 +25,7 @@ function ContextMenuGroup({
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
)
);
}
function ContextMenuPortal({
@ -33,13 +33,13 @@ function ContextMenuPortal({
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
)
);
}
function ContextMenuSub({
...props
}: 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({
@ -50,7 +50,7 @@ function ContextMenuRadioGroup({
data-slot="context-menu-radio-group"
{...props}
/>
)
);
}
function ContextMenuSubTrigger({
@ -59,22 +59,22 @@ function ContextMenuSubTrigger({
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.SubTrigger
data-slot="context-menu-sub-trigger"
data-inset={inset}
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",
className
"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,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</ContextMenuPrimitive.SubTrigger>
)
);
}
function ContextMenuSubContent({
@ -85,12 +85,12 @@ function ContextMenuSubContent({
<ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content"
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",
className
"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,
)}
{...props}
/>
)
);
}
function ContextMenuContent({
@ -102,13 +102,13 @@ function ContextMenuContent({
<ContextMenuPrimitive.Content
data-slot="context-menu-content"
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",
className
"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,
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
)
);
}
function ContextMenuItem({
@ -117,8 +117,8 @@ function ContextMenuItem({
variant = "default",
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<ContextMenuPrimitive.Item
@ -126,12 +126,12 @@ function ContextMenuItem({
data-inset={inset}
data-variant={variant}
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!",
className
"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,
)}
{...props}
/>
)
);
}
function ContextMenuCheckboxItem({
@ -144,8 +144,8 @@ function ContextMenuCheckboxItem({
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
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",
className
"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,
)}
checked={checked}
{...props}
@ -157,7 +157,7 @@ function ContextMenuCheckboxItem({
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
)
);
}
function ContextMenuRadioItem({
@ -169,8 +169,8 @@ function ContextMenuRadioItem({
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
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",
className
"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,
)}
{...props}
>
@ -181,7 +181,7 @@ function ContextMenuRadioItem({
</span>
{children}
</ContextMenuPrimitive.RadioItem>
)
);
}
function ContextMenuLabel({
@ -189,19 +189,19 @@ function ContextMenuLabel({
inset,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.Label
data-slot="context-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium text-foreground data-[inset]:pl-8",
className
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className,
)}
{...props}
/>
)
);
}
function ContextMenuSeparator({
@ -211,10 +211,10 @@ function ContextMenuSeparator({
return (
<ContextMenuPrimitive.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}
/>
)
);
}
function ContextMenuShortcut({
@ -225,12 +225,12 @@ function ContextMenuShortcut({
<span
data-slot="context-menu-shortcut"
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
)
);
}
export {
@ -249,4 +249,4 @@ export {
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}
};

View File

@ -102,7 +102,6 @@ export function DropdownSelector<T extends string>({
>
{/* {truncateMiddle(option.label,50)} */}
{truncateMiddle("hfiqwertyuiopasdfghjklxcvbnm.html", 20)}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>

View File

@ -1,9 +1,9 @@
"use client"
"use client";
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Slider({
className,
@ -20,8 +20,8 @@ function Slider({
: Array.isArray(defaultValue)
? defaultValue
: [min, max],
[value, defaultValue, min, max]
)
[value, defaultValue, min, max],
);
return (
<SliderPrimitive.Root
@ -32,20 +32,20 @@ function Slider({
max={max}
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",
className
className,
)}
{...props}
>
<SliderPrimitive.Track
data-slot="slider-track"
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
data-slot="slider-range"
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>
@ -53,11 +53,11 @@ function Slider({
<SliderPrimitive.Thumb
data-slot="slider-thumb"
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>
)
);
}
export { Slider }
export { Slider };

View File

@ -1,3 +1,4 @@
import ExcelJS from "exceljs";
import JSZip from "jszip";
import {
DownloadIcon,
@ -17,7 +18,6 @@ import {
} from "react";
import { toast } from "sonner";
import { Streamdown } from "streamdown";
import ExcelJS from "exceljs";
import {
Artifact,
@ -73,13 +73,11 @@ let revoGridLoaderPromise: Promise<void> | null = null;
function ensureRevoGridDefined() {
if (typeof window === "undefined") 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(window);
},
);
}
return revoGridLoaderPromise;
}
@ -99,18 +97,45 @@ function toGridCellText(cell: ExcelJS.Cell): string {
const value = cell.value;
if (value == null) return "";
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 ("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) {
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) {
return String(value.hyperlink);
const hyperlink = value.hyperlink;
if (typeof hyperlink === "string") {
return hyperlink;
}
}
return String(value);
}
return "";
}
function toRevoGridSheetData(worksheet: ExcelJS.Worksheet): RevoGridSheetData {
@ -469,7 +494,6 @@ export function ArtifactFileDetail({
{isWriteFile ? (
<div className="w-full overflow-hidden px-2 text-center text-ellipsis whitespace-nowrap">
{truncateMiddle(getFileName(filepath), 20)}
</div>
) : (
<DropdownSelector
@ -1703,12 +1727,18 @@ export const ArtifactZoomSelector = ({
</svg>
</button>
</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">
<span className="text-muted-foreground text-xs">
{ZOOM_LEVELS[0]}%
</span>
<span className="text-foreground text-xs font-medium">{value}%</span>
<span className="text-foreground text-xs font-medium">
{value}%
</span>
</div>
<Slider
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">
<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>
<main className="min-h-0 grow">
<ArtifactFileList

View File

@ -1,7 +1,5 @@
"use client";
import { useRouter } from "next/navigation";
import type { ChatStatus } from "ai";
import {
CheckIcon,
@ -15,6 +13,8 @@ import {
XIcon,
ZapIcon,
} 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 {
useCallback,
@ -26,6 +26,7 @@ import {
type KeyboardEvent,
type ComponentProps,
} from "react";
import { toast } from "sonner";
import {
PromptInput,
@ -71,15 +72,14 @@ import { useI18n } from "@/core/i18n/hooks";
import type { SelectedSkillPayloadItem } from "@/core/i18n/locales/types";
import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
import { useModels } from "@/core/models/hooks";
import type { AgentThreadContext } from "@/core/threads";
import {
MENTION_REFERENCE_EVENT,
type MentionReferenceEventDetail,
} from "@/core/threads/reference-events";
import type { AgentThreadContext } from "@/core/threads";
import { useUploadedFiles } from "@/core/uploads/hooks";
import { useIframeSkill } from "@/hooks/use-iframe-skill";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
import {
ModelSelector,
@ -91,12 +91,12 @@ import {
ModelSelectorTrigger,
} from "../ai-elements/model-selector";
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 { 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;
@ -373,7 +373,14 @@ export function InputBox({
});
setReferences([]);
},
[showWelcomeStyle, onSubmit, onStop, references, status, iframeSkill.selectedSkills],
[
showWelcomeStyle,
onSubmit,
onStop,
references,
status,
iframeSkill.selectedSkills,
],
);
const requestFormSubmit = useCallback(() => {
@ -381,7 +388,8 @@ export function InputBox({
form?.requestSubmit();
}, []);
const addMentionReference = useCallback((reference: PromptInputReference) => {
const addMentionReference = useCallback(
(reference: PromptInputReference) => {
setReferences((prev) => {
const exists = prev.some(
(item) =>
@ -398,7 +406,9 @@ export function InputBox({
}
return prev.concat(reference);
});
}, [t.inputBox.maxReferencesReached]);
},
[t.inputBox.maxReferencesReached],
);
const selectMentionCandidate = useCallback(
(candidate: MentionCandidate) => {
@ -433,7 +443,7 @@ export function InputBox({
useEffect(() => {
const onMentionReference = (event: Event) => {
const detail = (event as CustomEvent<MentionReferenceEventDetail>).detail;
if (!detail || detail.threadId !== threadIdFromProps) {
if (detail?.threadId !== threadIdFromProps) {
return;
}
addMentionReference({
@ -493,12 +503,13 @@ export function InputBox({
}
if (event.key === "ArrowDown") {
event.preventDefault();
setActiveMentionIndex((prev) =>
(prev + 1) % filteredMentionCandidates.length,
setActiveMentionIndex(
(prev) => (prev + 1) % filteredMentionCandidates.length,
);
} else if (event.key === "ArrowUp") {
event.preventDefault();
setActiveMentionIndex((prev) =>
setActiveMentionIndex(
(prev) =>
(prev - 1 + filteredMentionCandidates.length) %
filteredMentionCandidates.length,
);
@ -693,7 +704,7 @@ export function InputBox({
align="start"
side="top"
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"
onCloseAutoFocus={(event) => {
event.preventDefault();
@ -704,7 +715,7 @@ export function InputBox({
{t.inputBox.addReference}
</DropdownMenuLabel>
<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">
{filteredMentionCandidates.map((candidate, index) => {
const detail = [candidate.typeLabel, candidate.pathTail]
@ -717,7 +728,9 @@ export function InputBox({
"flex items-center justify-between gap-3 rounded-md px-2 py-2 text-left",
index === activeMentionIndex && "bg-accent",
)}
data-active={index === activeMentionIndex ? "true" : "false"}
data-active={
index === activeMentionIndex ? "true" : "false"
}
data-candidate-key={candidate.key}
data-testid="mention-candidate-item"
aria-label={`${candidate.filename} ${candidate.typeLabel}${candidate.pathTail ? ` ${candidate.pathTail}` : ""}`}
@ -781,11 +794,13 @@ export function InputBox({
/>
</PromptInputActionMenuContent>
</PromptInputActionMenu> */}
{showWelcomeStyle && <HistoryButton
{showWelcomeStyle && (
<HistoryButton
className="px-2!"
router={router}
threadId={threadIdFromProps}
/>}
/>
)}
<AddAttachmentsButton className="px-2!" />
<IframeSkillDialogButton
className="px-2!"
@ -1096,7 +1111,6 @@ function HistoryButton({
strokeLinejoin="round"
/>
</svg>
</PromptInputButton>
</Tooltip>
);
@ -1205,7 +1219,10 @@ function AttachmentPreviewBar({
</PromptInputAttachments>
)}
{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) => {
const referenceUrl =
threadId && reference.path
@ -1215,7 +1232,7 @@ function AttachmentPreviewBar({
})
: null;
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 mediaType = extension
? extension === "jpg"

View File

@ -41,8 +41,8 @@ function parseTableData(table: HTMLTableElement): TableData {
(cell.textContent ?? "").trim(),
);
const rows = Array.from(table.querySelectorAll("tbody tr")).map((row) =>
Array.from(row.querySelectorAll("td")).map(
(cell) => (cell.textContent ?? "").trim(),
Array.from(row.querySelectorAll("td")).map((cell) =>
(cell.textContent ?? "").trim(),
),
);
return { headers, rows };
@ -70,7 +70,9 @@ function MarkdownTable({
const handleCopy = useCallback(
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");
if (!(table instanceof HTMLTableElement)) return;
@ -89,10 +91,13 @@ function MarkdownTable({
);
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">
<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}
onClick={handleCopy}
title={copyLabel}
@ -103,7 +108,10 @@ function MarkdownTable({
</div>
<div className="overflow-x-auto">
<table
className={cn("w-full border-collapse border border-border", className)}
className={cn(
"border-border w-full border-collapse border",
className,
)}
data-streamdown="table"
{...props}
>

View File

@ -40,7 +40,6 @@ import { Tooltip } from "../tooltip";
import { MarkdownContent } from "./markdown-content";
export function MessageGroup({
className,
messages,
@ -87,11 +86,7 @@ export function MessageGroup({
const rehypePlugins = useRehypeSplitWordsIntoSpans(false);
const thinkingComponents = useMemo(
() => ({
code: ({
className,
children,
...props
}: ComponentProps<"code">) => {
code: ({ className, children, ...props }: ComponentProps<"code">) => {
const isBlock =
typeof className === "string" && className.includes("language-");
if (!isBlock) {
@ -126,7 +121,7 @@ export function MessageGroup({
<Button
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"
onClick={(event) => {
event.stopPropagation();

View File

@ -34,10 +34,10 @@ import {
stripUploadedFilesTag,
type FileInMessage,
} from "@/core/messages/utils";
import { dispatchMentionReference } from "@/core/threads/reference-events";
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
import { materializeSkillYaml } from "@/core/skills";
import { humanMessagePlugins } from "@/core/streamdown";
import { dispatchMentionReference } from "@/core/threads/reference-events";
import { cn } from "@/lib/utils";
import { CopyButton } from "../copy-button";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -164,7 +164,7 @@ export async function rewriteFirstReferenceAsArtifact(
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;
memoizedState?: unknown;

View File

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