feat(tour):漫游导航

This commit is contained in:
肖应宇 2026-04-20 15:31:52 +08:00
parent f0d93ab342
commit eb45bba7ff
6 changed files with 1060 additions and 150 deletions

View File

@ -58,6 +58,7 @@
"@uiw/react-codemirror": "^4.25.4", "@uiw/react-codemirror": "^4.25.4",
"@xyflow/react": "^12.10.0", "@xyflow/react": "^12.10.0",
"ai": "^6.0.33", "ai": "^6.0.33",
"antd": "^6.3.6",
"best-effort-json-parser": "^1.2.1", "best-effort-json-parser": "^1.2.1",
"better-auth": "^1.3", "better-auth": "^1.3",
"canvas-confetti": "^1.9.4", "canvas-confetti": "^1.9.4",

File diff suppressed because it is too large Load Diff

View File

@ -1064,7 +1064,7 @@ export const PromptInputTools = ({
className, className,
...props ...props
}: PromptInputToolsProps) => ( }: PromptInputToolsProps) => (
<div className={cn("flex items-center gap-1", className)} {...props} /> <div className={cn("flex items-center h-full gap-1", className)} {...props} />
); );
export type PromptInputButtonProps = ComponentProps<typeof InputGroupButton>; export type PromptInputButtonProps = ComponentProps<typeof InputGroupButton>;

View File

@ -37,7 +37,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
} }
const inputGroupAddonVariants = cva( const inputGroupAddonVariants = cva(
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50", "text-muted-foreground flex h-[58px] cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
{ {
variants: { variants: {
align: { align: {
@ -46,9 +46,9 @@ const inputGroupAddonVariants = cva(
"inline-end": "inline-end":
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]", "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
"block-start": "block-start":
"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5", "order-first w-full justify-start px-3 pt-5 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
"block-end": "block-end":
"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5", "order-last w-full justify-start px-3 py-0 pb-5 group-has-[>input]/input-group:pb-2.5",
}, },
}, },
defaultVariants: { defaultVariants: {

View File

@ -1,6 +1,7 @@
"use client"; "use client";
import type { ChatStatus } from "ai"; import type { ChatStatus } from "ai";
import { Tour } from "antd";
import { import {
CheckIcon, CheckIcon,
GraduationCapIcon, GraduationCapIcon,
@ -17,6 +18,7 @@ import type { AppRouterInstance } from "next/dist/shared/lib/app-router-context.
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { import {
forwardRef,
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
@ -25,6 +27,7 @@ import {
type ChangeEvent, type ChangeEvent,
type KeyboardEvent, type KeyboardEvent,
type ComponentProps, type ComponentProps,
type RefObject,
} from "react"; } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@ -99,6 +102,25 @@ import { Tooltip } from "./tooltip";
const MAX_REFERENCES_PER_MESSAGE = 10; const MAX_REFERENCES_PER_MESSAGE = 10;
const INPUT_TOOLS_TOUR_SEEN_KEY = "workspace.input_tools_tour_seen.v1";
type WorkspaceToolButtonProps = ComponentProps<typeof PromptInputButton>;
function WorkspaceToolButton({
className,
...props
}: WorkspaceToolButtonProps) {
return (
<PromptInputButton
className={cn(
// border border-[rgba(0,0,0,0.08)]
"group h-full p-[10px]! rounded-[10px] hover:bg-[#EAE2F5] hover:text-[#8E47F0]",
className,
)}
{...props}
/>
);
}
type MentionCandidate = { type MentionCandidate = {
key: string; key: string;
@ -215,6 +237,10 @@ export function InputBox({
const textareaRef = useRef<HTMLTextAreaElement | null>(null); const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
const mentionTriggerRef = useRef<HTMLButtonElement | null>(null); const mentionTriggerRef = useRef<HTMLButtonElement | null>(null);
const historyButtonTourRef = useRef<HTMLDivElement | null>(null);
const attachmentsButtonTourRef = useRef<HTMLDivElement | null>(null);
const skillButtonTourRef = useRef<HTMLDivElement | null>(null);
const suggestionListTourRef = useRef<HTMLDivElement | null>(null);
const [followups, setFollowups] = useState<string[]>([]); const [followups, setFollowups] = useState<string[]>([]);
const [followupsHidden, setFollowupsHidden] = useState(false); const [followupsHidden, setFollowupsHidden] = useState(false);
const [followupsLoading, setFollowupsLoading] = useState(false); const [followupsLoading, setFollowupsLoading] = useState(false);
@ -231,11 +257,97 @@ export function InputBox({
start: number; start: number;
end: number; end: number;
} | null>(null); } | null>(null);
const [isInputToolsTourOpen, setIsInputToolsTourOpen] = useState(false);
const [isInputToolsTourReady, setIsInputToolsTourReady] = useState(false);
const { data: uploadedFilesData } = useUploadedFiles(threadIdFromProps); const { data: uploadedFilesData } = useUploadedFiles(threadIdFromProps);
// isNewThread 时禁用收缩,始终保持展开(除非已提交消息) // isNewThread 时禁用收缩,始终保持展开(除非已提交消息)
const effectiveIsFocused = const effectiveIsFocused =
((showWelcomeStyle ?? false) && !hasSubmitted) || isFocused; ((showWelcomeStyle ?? false) && !hasSubmitted) || isFocused;
const shouldShowSuggestionList =
showWelcomeStyle && !hasSubmitted && searchParams.get("mode") !== "skill";
useEffect(() => {
if (!showWelcomeStyle || hasSubmitted) {
setIsInputToolsTourReady(false);
return;
}
const frameId = window.requestAnimationFrame(() => {
setIsInputToolsTourReady(
Boolean(
historyButtonTourRef.current &&
attachmentsButtonTourRef.current &&
skillButtonTourRef.current &&
(!shouldShowSuggestionList || suggestionListTourRef.current),
),
);
});
return () => window.cancelAnimationFrame(frameId);
}, [
showWelcomeStyle,
hasSubmitted,
shouldShowSuggestionList,
iframeSkill.isBootstrapping,
iframeSkill.selectedSkills.length,
]);
useEffect(() => {
if (!showWelcomeStyle || hasSubmitted || !isInputToolsTourReady) {
setIsInputToolsTourOpen(false);
return;
}
const hasSeenTour = window.localStorage.getItem(INPUT_TOOLS_TOUR_SEEN_KEY);
if (!hasSeenTour) {
setIsInputToolsTourOpen(true);
}
}, [showWelcomeStyle, hasSubmitted, isInputToolsTourReady]);
const closeInputToolsTour = useCallback(() => {
window.localStorage.setItem(INPUT_TOOLS_TOUR_SEEN_KEY, "1");
setIsInputToolsTourOpen(false);
}, []);
const inputToolsTourSteps = useMemo(() => {
const baseSteps = [
{
title: "查看历史",
description: "点击这里,可以查看历史会话与文档。",
target: () => historyButtonTourRef.current ?? document.body,
},
{
title: "上传附件",
description: "点击这里,上传参考文档或拟处理的文档。",
target: () => attachmentsButtonTourRef.current ?? document.body,
},
{
title: "选择 Skill",
description: (
<>
skill使skill
<br />
广skill使skill
</>
),
target: () => skillButtonTourRef.current ?? document.body,
},
...(shouldShowSuggestionList
? [
{
title: "试试我吧",
target: () => suggestionListTourRef.current ?? document.body,
},
]
: []),
];
return baseSteps.map((step, index) => ({
...step,
prevButtonProps: { children: "上一步" },
nextButtonProps: {
children: index === baseSteps.length - 1 ? "完成" : "下一步",
},
}));
}, [shouldShowSuggestionList]);
// 点击外部区域时收起输入框 // 点击外部区域时收起输入框
useEffect(() => { useEffect(() => {
@ -287,9 +399,9 @@ export function InputBox({
isImage: isImageFilename(filename), isImage: isImageFilename(filename),
previewUrl: threadId previewUrl: threadId
? urlOfArtifact({ ? urlOfArtifact({
filepath: path, filepath: path,
threadId, threadId,
}) })
: undefined, : undefined,
}; };
}); });
@ -615,6 +727,21 @@ export function InputBox({
}} }}
className="relative w-full" className="relative w-full"
> >
<Tour
open={isInputToolsTourOpen}
onClose={closeInputToolsTour}
onFinish={closeInputToolsTour}
gap={
{ offset: 4 , radius: 2 }
}
mask={{
style: {
boxShadow: 'inset 0 0 15px #333',
},
color: 'rgba(255,255,255, .8)',
}}
steps={inputToolsTourSteps}
/>
<AttachmentPreviewBar <AttachmentPreviewBar
references={references} references={references}
threadId={threadId} threadId={threadId}
@ -781,10 +908,10 @@ 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 w-full overflow-hidden gap-[20px]">
{/* TODO: Add more connectors here {/* TODO: Add more connectors here
<PromptInputActionMenu> <PromptInputActionMenu>
<PromptInputActionMenuTrigger className="px-2!" /> <PromptInputActionMenuTrigger className="px-2!" />
@ -795,20 +922,26 @@ export function InputBox({
</PromptInputActionMenuContent> </PromptInputActionMenuContent>
</PromptInputActionMenu> */} </PromptInputActionMenu> */}
{showWelcomeStyle && ( {showWelcomeStyle && (
<HistoryButton <div ref={historyButtonTourRef} className="shrink-0 h-full">
className="px-2!" <HistoryButton
router={router} router={router}
threadId={threadIdFromProps} threadId={threadIdFromProps}
/> />
</div>
)} )}
<AddAttachmentsButton className="px-2!" /> <div ref={attachmentsButtonTourRef} className="shrink-0 h-full">
<IframeSkillDialogButton <AddAttachmentsButton />
className="px-2!" </div>
selectedSkills={iframeSkill.selectedSkills} <div className="min-w-0 grow basis-0 h-full">
isBootstrapping={iframeSkill.isBootstrapping} <IframeSkillDialogButton
openSkillDialog={iframeSkill.openSkillDialog} skillButtonRef={skillButtonTourRef}
clearSkill={iframeSkill.clearSkill} selectedSkills={iframeSkill.selectedSkills}
/> isBootstrapping={iframeSkill.isBootstrapping}
openSkillDialog={iframeSkill.openSkillDialog}
clearSkill={iframeSkill.clearSkill}
/>
</div>
{/* <div className="h-[40px] w-[140px] shrink-0" aria-hidden="true" /> */}
{/* 参考 kexue 版本隐藏运行模式切换按钮 */} {/* 参考 kexue 版本隐藏运行模式切换按钮 */}
</PromptInputTools> </PromptInputTools>
@ -845,7 +978,7 @@ export function InputBox({
</ModelSelector> */} </ModelSelector> */}
<PromptInputTools> <PromptInputTools>
{/* 占位符 */} {/* 占位符 */}
<div className="w-[150px]"></div> <div className="w-[150px] h-[40px]"></div>
</PromptInputTools> </PromptInputTools>
</PromptInputFooter> </PromptInputFooter>
<PromptInputSubmit <PromptInputSubmit
@ -856,10 +989,9 @@ export function InputBox({
/> />
</PromptInput> </PromptInput>
{showWelcomeStyle && {shouldShowSuggestionList && (
!hasSubmitted &&
searchParams.get("mode") !== "skill" && (
<SuggestionListContainer <SuggestionListContainer
ref={suggestionListTourRef}
bootstrapAndLockSkills={iframeSkill.bootstrapAndLockSkills} bootstrapAndLockSkills={iframeSkill.bootstrapAndLockSkills}
isBootstrapping={iframeSkill.isBootstrapping} isBootstrapping={iframeSkill.isBootstrapping}
/> />
@ -926,25 +1058,29 @@ export function InputBox({
} }
// SuggestionList 容器 // SuggestionList 容器
function SuggestionListContainer({ const SuggestionListContainer = forwardRef<HTMLDivElement, {
bootstrapAndLockSkills,
isBootstrapping,
}: {
bootstrapAndLockSkills: (params: { bootstrapAndLockSkills: (params: {
selectedSkills: SelectedSkillPayloadItem[]; selectedSkills: SelectedSkillPayloadItem[];
title: string; title: string;
}) => Promise<boolean>; }) => Promise<boolean>;
isBootstrapping: boolean; isBootstrapping: boolean;
}) { }>(
return ( function SuggestionListContainer(
{ bootstrapAndLockSkills, isBootstrapping },
ref,
) {
return (
<div className="absolute right-0 bottom-0 left-0 z-0 flex translate-y-full items-center justify-center pt-4"> <div className="absolute right-0 bottom-0 left-0 z-0 flex translate-y-full items-center justify-center pt-4">
<div ref={ref} className="w-fit">
<SuggestionList <SuggestionList
bootstrapAndLockSkills={bootstrapAndLockSkills} bootstrapAndLockSkills={bootstrapAndLockSkills}
isBootstrapping={isBootstrapping} isBootstrapping={isBootstrapping}
/> />
</div>
</div> </div>
); );
} },
);
// 快速选择skillbutton // 快速选择skillbutton
function SuggestionList({ function SuggestionList({
@ -1046,28 +1182,28 @@ function AddAttachmentsButton({ className }: { className?: string }) {
const attachments = usePromptInputAttachments(); const attachments = usePromptInputAttachments();
return ( return (
<Tooltip content={t.inputBox.addAttachments}> <Tooltip content={t.inputBox.addAttachments}>
<PromptInputButton <WorkspaceToolButton
className={cn("group px-2! hover:bg-[#EAE2F5]", className)} className={className}
onClick={() => attachments.openFileDialog()} onClick={() => attachments.openFileDialog()}
>
<svg
width="18"
height="15"
viewBox="0 0 18 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="transition-[stroke] duration-200 [&>path]:transition-[fill,stroke] [&>path]:duration-200 [&>path:first-child]:group-hover:fill-[#8E47F0] [&>path:last-child]:group-hover:stroke-[#8E47F0]"
> >
<svg <path
width="18" d="M7.05042 7.65254C6.9754 7.72756 6.90039 7.80257 6.90039 7.95258C6.90039 8.02759 6.9754 8.1776 7.05042 8.25262C7.20043 8.40263 7.42545 8.40263 7.57546 8.25262L8.8506 6.97747V10.7279C8.8506 10.9529 9.00061 11.1029 9.22563 11.1029C9.30065 11.1029 9.45066 11.0279 9.52567 11.0279C9.60067 10.9529 9.67568 10.8779 9.67568 10.7279V6.97747L10.9508 8.25262C11.1008 8.40263 11.3259 8.40263 11.4759 8.25262C11.5509 8.1776 11.6259 8.10259 11.6259 7.95258C11.6259 7.87757 11.5509 7.72756 11.4759 7.65254L9.52567 5.70235C9.37564 5.55234 9.15062 5.55234 9.00061 5.70235L7.05042 7.65254Z"
height="15" fill="#150033"
viewBox="0 0 18 15" />
fill="none" <path
xmlns="http://www.w3.org/2000/svg" d="M1.12695 0.5H6.67871C6.87077 0.500077 7.01409 0.574515 7.07324 0.648438L7.09082 0.669922L8.30762 1.88672C8.6222 2.20119 9.01344 2.3681 9.44629 2.36816H16.875C17.2382 2.36842 17.5012 2.63339 17.5 2.99414V13.8848C17.5048 14.2408 17.2454 14.5056 16.8818 14.5059H1.12695C0.764649 14.5057 0.5 14.2401 0.5 13.877V1.12793C0.500049 0.810129 0.702664 0.567404 0.996094 0.511719L1.12695 0.5Z"
className="transition-[stroke] duration-200 [&>path]:transition-[fill,stroke] [&>path]:duration-200 [&>path:first-child]:group-hover:fill-[#8E47F0] [&>path:last-child]:group-hover:stroke-[#8E47F0]" stroke="#150033"
> />
<path </svg>
d="M7.05042 7.65254C6.9754 7.72756 6.90039 7.80257 6.90039 7.95258C6.90039 8.02759 6.9754 8.1776 7.05042 8.25262C7.20043 8.40263 7.42545 8.40263 7.57546 8.25262L8.8506 6.97747V10.7279C8.8506 10.9529 9.00061 11.1029 9.22563 11.1029C9.30065 11.1029 9.45066 11.0279 9.52567 11.0279C9.60067 10.9529 9.67568 10.8779 9.67568 10.7279V6.97747L10.9508 8.25262C11.1008 8.40263 11.3259 8.40263 11.4759 8.25262C11.5509 8.1776 11.6259 8.10259 11.6259 7.95258C11.6259 7.87757 11.5509 7.72756 11.4759 7.65254L9.52567 5.70235C9.37564 5.55234 9.15062 5.55234 9.00061 5.70235L7.05042 7.65254Z" </WorkspaceToolButton>
fill="#150033"
/>
<path
d="M1.12695 0.5H6.67871C6.87077 0.500077 7.01409 0.574515 7.07324 0.648438L7.09082 0.669922L8.30762 1.88672C8.6222 2.20119 9.01344 2.3681 9.44629 2.36816H16.875C17.2382 2.36842 17.5012 2.63339 17.5 2.99414V13.8848C17.5048 14.2408 17.2454 14.5056 16.8818 14.5059H1.12695C0.764649 14.5057 0.5 14.2401 0.5 13.877V1.12793C0.500049 0.810129 0.702664 0.567404 0.996094 0.511719L1.12695 0.5Z"
stroke="#150033"
/>
</svg>
</PromptInputButton>
</Tooltip> </Tooltip>
); );
} }
@ -1084,46 +1220,48 @@ function HistoryButton({
const { t } = useI18n(); const { t } = useI18n();
return ( return (
<Tooltip content={t.inputBox.history}> <Tooltip content={t.inputBox.history}>
<PromptInputButton <WorkspaceToolButton
className={cn("group px-2! hover:bg-[#EAE2F5]", className)} className={className}
onClick={() => onClick={() =>
router.replace(`/workspace/chats/${threadId}?is_chatting=true`) router.replace(`/workspace/chats/${threadId}?is_chatting=true`)
} }
>
<svg
className="transition-[stroke] duration-200"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
> >
<svg <circle
className="transition-[stroke] duration-200" className="stroke-[#150033] transition-[stroke] duration-200 group-hover:stroke-[#8E47F0]"
width="18" cx="9"
height="18" cy="9"
viewBox="0 0 18 18" r="8.5"
fill="none" />
xmlns="http://www.w3.org/2000/svg" <path
> className="stroke-[#150033] transition-[stroke] duration-200 group-hover:stroke-[#8E47F0]"
<circle d="M9 6V10H12"
className="stroke-[#150033] transition-[stroke] duration-200 group-hover:stroke-[#8E47F0]" strokeLinecap="round"
cx="9" strokeLinejoin="round"
cy="9" />
r="8.5" </svg>
/> </WorkspaceToolButton>
<path
className="stroke-[#150033] transition-[stroke] duration-200 group-hover:stroke-[#8E47F0]"
d="M9 6V10H12"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</PromptInputButton>
</Tooltip> </Tooltip>
); );
} }
// 启动iframeSkillDialog // 启动iframeSkillDialog
function IframeSkillDialogButton({ function IframeSkillDialogButton({
className, className,
skillButtonRef,
selectedSkills, selectedSkills,
isBootstrapping, isBootstrapping,
openSkillDialog, openSkillDialog,
clearSkill, clearSkill,
}: { }: {
className?: string; className?: string;
skillButtonRef?: RefObject<HTMLDivElement | null>;
selectedSkills: Array<{ skill_id: string; title: string }>; selectedSkills: Array<{ skill_id: string; title: string }>;
isBootstrapping: boolean; isBootstrapping: boolean;
openSkillDialog: () => void; openSkillDialog: () => void;
@ -1132,24 +1270,26 @@ function IframeSkillDialogButton({
const { t } = useI18n(); const { t } = useI18n();
return ( return (
<div className="flex min-w-0 flex-1 items-center gap-2"> <div className="flex min-w-0 w-full items-center h-full gap-2">
<Tooltip content={t.inputBox.selectSkill}> <Tooltip content={t.inputBox.selectSkill}>
<PromptInputButton <div ref={skillButtonRef} className="shrink-0">
className={cn("group shrink-0 px-2! hover:bg-[#EAE2F5]", className)} <WorkspaceToolButton
onClick={openSkillDialog} className={cn("shrink-0", className)}
> onClick={openSkillDialog}
<svg
xmlns="http://www.w3.org/2000/svg"
className="size-4 transition-[stroke] duration-200 [&>path]:transition-[stroke] [&>path]:duration-200 [&>path]:group-hover:stroke-[#8E47F0]"
viewBox="0 0 12 16"
fill="none"
> >
<path <svg
d="M3.7998 0.5H9.19922C9.24033 0.5 9.26852 0.518136 9.28516 0.541992C9.30124 0.565318 9.30411 0.588767 9.29395 0.613281H9.29297L7.43066 5.07422L7.1416 5.76758H11.3994C11.4295 5.76765 11.4474 5.77552 11.459 5.7832C11.4724 5.79207 11.4846 5.80503 11.4922 5.82129C11.4997 5.83745 11.5013 5.85253 11.5 5.86328C11.4989 5.87156 11.4953 5.88556 11.4785 5.9043L2.87891 15.4629V15.4639C2.85396 15.4914 2.83406 15.4971 2.82031 15.499C2.80144 15.5016 2.77553 15.4981 2.74902 15.4844C2.72225 15.4705 2.70837 15.453 2.70312 15.4424C2.70056 15.4372 2.69457 15.4253 2.70312 15.3936V15.3926L4.30273 9.49512L4.47461 8.86426H0.600586C0.559682 8.86424 0.531324 8.84587 0.514648 8.82227C0.498608 8.79944 0.496551 8.777 0.505859 8.75293L3.70508 0.558594C3.71075 0.544183 3.72173 0.529788 3.73828 0.518555C3.74688 0.51277 3.75704 0.508037 3.76758 0.504883L3.7998 0.5Z" xmlns="http://www.w3.org/2000/svg"
stroke="#150033" className="size-4 transition-[stroke] duration-200 [&>path]:transition-[stroke] [&>path]:duration-200 [&>path]:group-hover:stroke-[#8E47F0]"
/> viewBox="0 0 12 16"
</svg> fill="none"
</PromptInputButton> >
<path
d="M3.7998 0.5H9.19922C9.24033 0.5 9.26852 0.518136 9.28516 0.541992C9.30124 0.565318 9.30411 0.588767 9.29395 0.613281H9.29297L7.43066 5.07422L7.1416 5.76758H11.3994C11.4295 5.76765 11.4474 5.77552 11.459 5.7832C11.4724 5.79207 11.4846 5.80503 11.4922 5.82129C11.4997 5.83745 11.5013 5.85253 11.5 5.86328C11.4989 5.87156 11.4953 5.88556 11.4785 5.9043L2.87891 15.4629V15.4639C2.85396 15.4914 2.83406 15.4971 2.82031 15.499C2.80144 15.5016 2.77553 15.4981 2.74902 15.4844C2.72225 15.4705 2.70837 15.453 2.70312 15.4424C2.70056 15.4372 2.69457 15.4253 2.70312 15.3936V15.3926L4.30273 9.49512L4.47461 8.86426H0.600586C0.559682 8.86424 0.531324 8.84587 0.514648 8.82227C0.498608 8.79944 0.496551 8.777 0.505859 8.75293L3.70508 0.558594C3.71075 0.544183 3.72173 0.529788 3.73828 0.518555C3.74688 0.51277 3.75704 0.508037 3.76758 0.504883L3.7998 0.5Z"
stroke="#150033"
/>
</svg>
</WorkspaceToolButton>
</div>
</Tooltip> </Tooltip>
{isBootstrapping ? ( {isBootstrapping ? (
<Tag className="bg-background text-muted-foreground gap-2 border"> <Tag className="bg-background text-muted-foreground gap-2 border">
@ -1159,7 +1299,7 @@ function IframeSkillDialogButton({
) : null} ) : null}
{!isBootstrapping && selectedSkills.length > 0 ? ( {!isBootstrapping && selectedSkills.length > 0 ? (
<div <div
className="flex min-w-0 flex-1 items-center gap-2 overflow-x-auto overflow-y-hidden whitespace-nowrap [scrollbar-width:none] [&::-webkit-scrollbar]:hidden" className="flex min-w-0 grow basis-0 items-center gap-2 overflow-x-auto overflow-y-hidden whitespace-nowrap [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
onWheel={(event) => { onWheel={(event) => {
if (event.deltaY === 0) return; if (event.deltaY === 0) return;
event.currentTarget.scrollLeft += event.deltaY; event.currentTarget.scrollLeft += event.deltaY;
@ -1227,9 +1367,9 @@ function AttachmentPreviewBar({
const referenceUrl = const referenceUrl =
threadId && reference.path threadId && reference.path
? urlOfArtifact({ ? urlOfArtifact({
filepath: reference.path, filepath: reference.path,
threadId, threadId,
}) })
: null; : null;
const filename = reference.filename ?? "reference"; const filename = reference.filename ?? "reference";
const imageMatch = /\.(png|jpe?g|gif|webp|bmp|svg)$/i.exec(filename); const imageMatch = /\.(png|jpe?g|gif|webp|bmp|svg)$/i.exec(filename);

View File

@ -79,7 +79,7 @@ export const zhCN: Translations = {
// Input Box // Input Box
inputBox: { inputBox: {
placeholder: "可直接对话; 或输入需求并选择skill完成专业任务;", placeholder: "可直接对话; 或输入需求并选择skill完成专业任务;",
welcomePlaceholder: "可直接对话; 或输入需求并选择skill完成专业任务;", welcomePlaceholder: "可直接对话; 或输入需求并选择skill完成专业任务",
chatPlaceholder: "“@”可引用文件。", chatPlaceholder: "“@”可引用文件。",
createSkillPrompt: createSkillPrompt:
"我们一起用 skill-creator 技能来创建一个技能吧。先问问我希望这个技能能做什么。", "我们一起用 skill-creator 技能来创建一个技能吧。先问问我希望这个技能能做什么。",