Compare commits

...

3 Commits

14 changed files with 97 additions and 103 deletions

View File

@ -6,7 +6,6 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { ConversationEmptyState } from "@/components/ai-elements/conversation"; import { ConversationEmptyState } from "@/components/ai-elements/conversation";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { import {
DevDialog, DevDialog,
DevDialogContent, DevDialogContent,
@ -27,7 +26,6 @@ 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";
import { ThreadTitle } from "@/components/workspace/thread-title"; import { ThreadTitle } from "@/components/workspace/thread-title";
import { TokenUsageIndicator } from "@/components/workspace/token-usage-indicator";
import { Tooltip } from "@/components/workspace/tooltip"; import { Tooltip } from "@/components/workspace/tooltip";
import { useSpecificChatMode } from "@/components/workspace/use-chat-mode"; import { useSpecificChatMode } from "@/components/workspace/use-chat-mode";
import { Welcome } from "@/components/workspace/welcome"; import { Welcome } from "@/components/workspace/welcome";
@ -260,7 +258,6 @@ export default function ChatPage() {
)} )}
</div> </div>
<div className="flex items-center justify-end gap-2 overflow-hidden"> <div className="flex items-center justify-end gap-2 overflow-hidden">
{/* <TokenUsageIndicator messages={thread.messages} /> */}
<DevTodoList <DevTodoList
className="bg-white" className="bg-white"
todos={thread.values.todos ?? []} todos={thread.values.todos ?? []}
@ -521,21 +518,3 @@ export default function ChatPage() {
</ThreadContext.Provider> </ThreadContext.Provider>
); );
} }
function InputBoxSkeleton() {
return (
<div className="w-full rounded-[20px] bg-[#FBFAFC] p-4 shadow-[0_0_20px_0_rgba(0,0,0,0.10)]">
<div className="flex flex-col gap-4">
<Skeleton className="h-6 w-[220px]" />
<Skeleton className="h-[120px] w-full rounded-[16px]" />
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Skeleton className="h-9 w-9 rounded-full" />
<Skeleton className="h-9 w-9 rounded-full" />
</div>
<Skeleton className="h-9 w-20 rounded-full" />
</div>
</div>
</div>
);
}

View File

@ -9,9 +9,9 @@ import {
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import { Toaster } from "@/components/ui/sonner";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import { Toaster } from "@/components/ui/sonner";
import { CommandPalette } from "@/components/workspace/command-palette"; import { CommandPalette } from "@/components/workspace/command-palette";
import { WorkspaceSidebar } from "@/components/workspace/workspace-sidebar"; import { WorkspaceSidebar } from "@/components/workspace/workspace-sidebar";
import { getLocalSettings, useLocalSettings } from "@/core/settings"; import { getLocalSettings, useLocalSettings } from "@/core/settings";

View File

@ -140,8 +140,8 @@ export const ArtifactContent = ({
className, className,
...props ...props
}: ArtifactContentProps) => ( }: ArtifactContentProps) => (
<div className="min-h-0 flex-1 overflow-auto rounded-[10px]"> <div className="min-h-0 flex-1 overflow-auto rounded-[10px]" {...props} >
{/* <div className={cn("mb-[207px]! p-4", className)} {...props} /> */} {/* <div className={cn("mb-[207px]! p-4", className)} {...props} /> */}
<div className={cn("mb-[150px] min-h-full p-4", className)} {...props} /> {/* <div className={cn("mb-[150px] min-h-full p-4", className)} /> */}
</div> </div>
); );

View File

@ -34,7 +34,6 @@ import { CodeEditor } from "@/components/workspace/code-editor";
import { useArtifactContent } from "@/core/artifacts/hooks"; import { useArtifactContent } from "@/core/artifacts/hooks";
import { urlOfArtifact } from "@/core/artifacts/utils"; import { urlOfArtifact } from "@/core/artifacts/utils";
import { useI18n } from "@/core/i18n/hooks"; import { useI18n } from "@/core/i18n/hooks";
import { installSkill } from "@/core/skills/api";
import { streamdownPlugins } from "@/core/streamdown"; import { streamdownPlugins } from "@/core/streamdown";
import { checkCodeFile, getFileName } from "@/core/utils/files"; import { checkCodeFile, getFileName } from "@/core/utils/files";
import { useMarkdownDownload } from "@/core/utils/markdown-download"; import { useMarkdownDownload } from "@/core/utils/markdown-download";
@ -61,7 +60,7 @@ export function ArtifactFileDetail({
}: { }: {
className?: string; className?: string;
filepath: string; filepath: string;
threadId: string; threadId?: string;
}) { }) {
const { t } = useI18n(); const { t } = useI18n();
const { artifacts, setOpen, select, fullscreen, setFullscreen } = const { artifacts, setOpen, select, fullscreen, setFullscreen } =
@ -98,12 +97,18 @@ export function ArtifactFileDetail({
return (language === "html" && !isWriteFile) || language === "markdown"; return (language === "html" && !isWriteFile) || language === "markdown";
}, [isWriteFile, language]); }, [isWriteFile, language]);
const artifactUrl = useMemo(() => { const artifactUrl = useMemo(() => {
if (!threadId) {
return "";
}
return urlOfArtifact({ filepath, threadId }); return urlOfArtifact({ filepath, threadId });
}, [filepath, threadId]); }, [filepath, threadId]);
const artifactPreviewKind = useMemo(() => { const artifactPreviewKind = useMemo(() => {
return getArtifactPreviewKind(filepath); return getArtifactPreviewKind(filepath);
}, [filepath]); }, [filepath]);
const artifactViewerSrcDoc = useMemo(() => { const artifactViewerSrcDoc = useMemo(() => {
if (!artifactUrl) {
return "";
}
return buildArtifactViewerSrcDoc({ return buildArtifactViewerSrcDoc({
artifactUrl, artifactUrl,
fileName, fileName,
@ -113,7 +118,7 @@ export function ArtifactFileDetail({
const { content } = useArtifactContent({ const { content } = useArtifactContent({
threadId, threadId,
filepath: filepathFromProps, filepath: filepathFromProps,
enabled: isCodeFile && !isWriteFile, enabled: Boolean(threadId) && isCodeFile && !isWriteFile,
}); });
const displayContent = content ?? ""; const displayContent = content ?? "";
@ -126,7 +131,6 @@ export function ArtifactFileDetail({
}, [artifacts]); }, [artifacts]);
const [viewMode, setViewMode] = useState<"code" | "preview">("code"); const [viewMode, setViewMode] = useState<"code" | "preview">("code");
const [isInstalling, setIsInstalling] = useState(false);
const [zoom, setZoom] = useState(80); const [zoom, setZoom] = useState(80);
// 是否可以转换为docx/pdf仅markdown文件支持 // 是否可以转换为docx/pdf仅markdown文件支持
@ -172,33 +176,11 @@ export function ArtifactFileDetail({
} }
}, [previewable]); }, [previewable]);
const handleInstallSkill = useCallback(async () => {
if (isInstalling) return;
setIsInstalling(true);
try {
const result = await installSkill({
thread_id: threadId,
path: filepath,
});
if (result.success) {
toast.success(result.message);
} else {
toast.error(result.message ?? "Failed to install skill");
}
} catch (error) {
console.error("Failed to install skill:", error);
toast.error("Failed to install skill");
} finally {
setIsInstalling(false);
}
}, [threadId, filepath, isInstalling]);
return ( return (
// 给滚动遮挡头部定位relative // 给滚动遮挡头部定位relative
<Artifact <Artifact
className={cn( className={cn(
"bg-background relative overflow-hidden rounded-2xl", "bg-background relative h-full overflow-hidden rounded-2xl",
className, className,
)} )}
> >
@ -358,7 +340,7 @@ export function ArtifactFileDetail({
<a <a
href={urlOfArtifact({ href={urlOfArtifact({
filepath, filepath,
threadId, threadId: threadId ?? "",
download: true, download: true,
})} })}
target="_blank" target="_blank"
@ -480,33 +462,39 @@ export function ArtifactFileDetail({
</ArtifactActions> </ArtifactActions>
</div> </div>
</ArtifactHeader> </ArtifactHeader>
<ArtifactContent className="rounded-b-[10px] bg-white p-0"> <ArtifactContent>
{/* 遮挡多余的滚动顶部 */} {/* 遮挡多余的滚动顶部 */}
{/* <div className="absolute w-[calc(100%-40px)] bg-white z-20 h-5 rounded-t-[10px] top-[57px]"></div> */} {/* <div className="absolute w-[calc(100%-40px)] bg-white z-20 h-5 rounded-t-[10px] top-[57px]"></div> */}
{previewable && {previewable &&
viewMode === "preview" && viewMode === "preview" &&
(language === "markdown" || language === "html") && ( (language === "markdown" || language === "html") && (
<div className="min-h-full mb-[150px] rounded-b-[10px] bg-white p-0 mb-0">
<ArtifactFilePreview <ArtifactFilePreview
content={displayContent} content={displayContent}
language={language ?? "text"} language={language ?? "text"}
zoom={zoom} zoom={zoom}
/> />
</div>
)} )}
{isCodeFile && viewMode === "code" && ( {isCodeFile && viewMode === "code" && (
<div className="min-h-full mb-[150px] rounded-b-[10px] bg-white p-0 mb-0">
<CodeEditor <CodeEditor
className="size-full resize-none rounded-none border-none py-[20px]" className="size-full resize-none rounded-none border-none py-[20px]"
value={displayContent ?? ""} value={displayContent ?? ""}
zoom={zoom} zoom={zoom}
readonly readonly
/> />
</div>
)} )}
{!isCodeFile && ( {!isCodeFile && (
<div className="h-full mb-[150px] ">
<iframe <iframe
className="size-full border-0" className="size-full border-0"
srcDoc={artifactViewerSrcDoc} srcDoc={artifactViewerSrcDoc}
sandbox="allow-same-origin allow-scripts allow-downloads" sandbox="allow-same-origin allow-scripts allow-downloads"
title={`Artifact preview: ${fileName}`} title={`Artifact preview: ${fileName}`}
/> />
</div>
)} )}
</ArtifactContent> </ArtifactContent>
</Artifact> </Artifact>
@ -527,7 +515,7 @@ export function ArtifactFilePreview({
if (language === "markdown") { if (language === "markdown") {
return ( return (
<div <div
className={cn("w-full p-[20px]")} className={cn("w-full mb-[207px] p-[20px]")}
style={{ "--zoom-scale": zoomScale } as React.CSSProperties} style={{ "--zoom-scale": zoomScale } as React.CSSProperties}
> >
<Streamdown <Streamdown
@ -617,6 +605,8 @@ function buildArtifactViewerSrcDoc({
</div>`; </div>`;
})(); })();
const bodyClass = kind === "image" ? "fullbleed" : "";
return `<!doctype html> return `<!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
@ -648,8 +638,12 @@ function buildArtifactViewerSrcDoc({
overflow: hidden; overflow: hidden;
padding: 12px; padding: 12px;
} }
body.fullbleed {
padding: 0;
}
.preview { .preview {
width: 100%; width: 100%;
height: 100%;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: var(--radius); border-radius: var(--radius);
background: var(--panel); background: var(--panel);
@ -715,7 +709,7 @@ function buildArtifactViewerSrcDoc({
.link:hover { text-decoration: underline; } .link:hover { text-decoration: underline; }
</style> </style>
</head> </head>
<body> <body class="${bodyClass}">
${content} ${content}
</body> </body>
</html>`; </html>`;

View File

@ -29,7 +29,7 @@ export function ArtifactFileList({
}: { }: {
className?: string; className?: string;
files: string[]; files: string[];
threadId: string; threadId?: string;
}) { }) {
const { t } = useI18n(); const { t } = useI18n();
const { select: selectArtifact, setOpen } = useArtifacts(); const { select: selectArtifact, setOpen } = useArtifacts();
@ -48,6 +48,7 @@ export function ArtifactFileList({
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
if (!threadId) return;
if (installingFile) return; if (installingFile) return;
setInstallingFile(filepath); setInstallingFile(filepath);
@ -98,7 +99,7 @@ export function ArtifactFileList({
{file.endsWith(".skill") && ( {file.endsWith(".skill") && (
<Button <Button
variant="ghost" variant="ghost"
disabled={installingFile === file} disabled={!threadId || installingFile === file}
onClick={(e) => handleInstallSkill(e, file)} onClick={(e) => handleInstallSkill(e, file)}
> >
{installingFile === file ? ( {installingFile === file ? (
@ -109,10 +110,11 @@ export function ArtifactFileList({
{t.common.install} {t.common.install}
</Button> </Button>
)} )}
{threadId ? (
<a <a
href={urlOfArtifact({ href={urlOfArtifact({
filepath: file, filepath: file,
threadId: threadId, threadId,
download: true, download: true,
})} })}
target="_blank" target="_blank"
@ -123,6 +125,12 @@ export function ArtifactFileList({
{t.common.download} {t.common.download}
</Button> </Button>
</a> </a>
) : (
<Button variant="ghost" disabled>
<DownloadIcon className="size-4" />
{t.common.download}
</Button>
)}
</CardAction> </CardAction>
</CardHeader> </CardHeader>
</Card> </Card>

View File

@ -23,7 +23,10 @@ import { useThread } from "../messages/context";
const CLOSE_MODE = { chat: 100, artifacts: 0 }; const CLOSE_MODE = { chat: 100, artifacts: 0 };
const OPEN_MODE = { chat: 60, artifacts: 40 }; const OPEN_MODE = { chat: 60, artifacts: 40 };
const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({ const ChatBox: React.FC<{
children: React.ReactNode;
threadId: string | undefined;
}> = ({
children, children,
threadId, threadId,
}) => { }) => {

View File

@ -21,7 +21,7 @@ import type { AgentThread } from "@/core/threads/types";
import { useThread } from "./messages/context"; import { useThread } from "./messages/context";
import { Tooltip } from "./tooltip"; import { Tooltip } from "./tooltip";
export function ExportTrigger({ threadId }: { threadId: string }) { export function ExportTrigger({ threadId }: { threadId?: string }) {
const { t } = useI18n(); const { t } = useI18n();
const { thread } = useThread(); const { thread } = useThread();
@ -49,7 +49,7 @@ export function ExportTrigger({ threadId }: { threadId: string }) {
[messages, thread.values, threadId, t], [messages, thread.values, threadId, t],
); );
if (messages.length === 0) { if (!threadId || messages.length === 0) {
return null; return null;
} }

View File

@ -39,7 +39,7 @@ export function MessageList({
paddingBottom = 160, paddingBottom = 160,
}: { }: {
className?: string; className?: string;
threadId: string; threadId?: string;
thread: UseStream<AgentThreadState>; thread: UseStream<AgentThreadState>;
/** When set (e.g. from onFinish), use instead of thread.messages so SSE end shows complete state. */ /** When set (e.g. from onFinish), use instead of thread.messages so SSE end shows complete state. */
messagesOverride?: Message[]; messagesOverride?: Message[];
@ -98,7 +98,9 @@ export function MessageList({
className="mb-4" className="mb-4"
/> />
)} )}
{threadId ? (
<ArtifactFileList files={files} threadId={threadId} /> <ArtifactFileList files={files} threadId={threadId} />
) : null}
</div> </div>
); );
} else if (group.type === "assistant:subagent") { } else if (group.type === "assistant:subagent") {

View File

@ -40,6 +40,7 @@ export function ThreadTitle({
t.pages.newChat, t.pages.newChat,
t.pages.untitled, t.pages.untitled,
t.pages.appName, t.pages.appName,
thread,
thread?.isThreadLoading, thread?.isThreadLoading,
thread?.values, thread?.values,
]); ]);

View File

@ -11,7 +11,7 @@ export function useArtifactContent({
enabled, enabled,
}: { }: {
filepath: string; filepath: string;
threadId: string; threadId?: string;
enabled?: boolean; enabled?: boolean;
}) { }) {
const isWriteFile = useMemo(() => { const isWriteFile = useMemo(() => {
@ -25,12 +25,17 @@ export function useArtifactContent({
return null; return null;
}, [filepath, isWriteFile, thread]); }, [filepath, isWriteFile, thread]);
const canFetch = Boolean(threadId) && enabled !== false;
const { data, isLoading, error } = useQuery({ const { data, isLoading, error } = useQuery({
queryKey: ["artifact", filepath, threadId, isMock], queryKey: ["artifact", filepath, threadId, isMock],
queryFn: () => { queryFn: () => {
return loadArtifactContent({ filepath, threadId, isMock }); return loadArtifactContent({
filepath,
threadId: threadId ?? "",
isMock,
});
}, },
enabled, enabled: canFetch,
// Cache artifact content for 5 minutes to avoid repeated fetches (especially for .skill ZIP extraction) // Cache artifact content for 5 minutes to avoid repeated fetches (especially for .skill ZIP extraction)
staleTime: 5 * 60 * 1000, staleTime: 5 * 60 * 1000,
}); });

View File

@ -1,6 +1,5 @@
import { import {
CompassIcon, CompassIcon,
GraduationCapIcon,
ImageIcon, ImageIcon,
MicroscopeIcon, MicroscopeIcon,
PenLineIcon, PenLineIcon,

View File

@ -271,7 +271,7 @@ export function useThreadStream({
const sendMessage = useCallback( const sendMessage = useCallback(
async ( async (
threadId: string, threadId: string | undefined,
message: PromptInputMessage, message: PromptInputMessage,
extraContext?: Record<string, unknown>, extraContext?: Record<string, unknown>,
) => { ) => {
@ -281,6 +281,8 @@ export function useThreadStream({
sendInFlightRef.current = true; sendInFlightRef.current = true;
const text = message.text.trim(); const text = message.text.trim();
const resolvedThreadId =
threadId ?? threadIdRef.current ?? undefined;
// Capture current count before showing optimistic messages // Capture current count before showing optimistic messages
prevMsgCountRef.current = thread.messages.length; prevMsgCountRef.current = thread.messages.length;
@ -315,7 +317,9 @@ export function useThreadStream({
} }
setOptimisticMessages(newOptimistic); setOptimisticMessages(newOptimistic);
_handleOnStart(threadId); if (resolvedThreadId) {
_handleOnStart(resolvedThreadId);
}
let uploadedFileInfo: UploadedFileInfo[] = []; let uploadedFileInfo: UploadedFileInfo[] = [];
@ -359,12 +363,12 @@ export function useThreadStream({
); );
} }
if (!threadId) { if (!resolvedThreadId) {
throw new Error("Thread is not ready for file upload."); throw new Error("Thread is not ready for file upload.");
} }
if (files.length > 0) { if (files.length > 0) {
const uploadResponse = await uploadFiles(threadId, files); const uploadResponse = await uploadFiles(resolvedThreadId, files);
uploadedFileInfo = uploadResponse.files; uploadedFileInfo = uploadResponse.files;
// Update optimistic human message with uploaded status + paths // Update optimistic human message with uploaded status + paths
@ -431,7 +435,7 @@ export function useThreadStream({
], ],
}, },
{ {
threadId: threadId, threadId: resolvedThreadId,
streamSubgraphs: true, streamSubgraphs: true,
streamResumable: true, streamResumable: true,
config: { config: {
@ -452,7 +456,7 @@ export function useThreadStream({
: context.mode === "thinking" : context.mode === "thinking"
? "low" ? "low"
: undefined), : undefined),
thread_id: threadId, ...(resolvedThreadId ? { thread_id: resolvedThreadId } : {}),
}, },
}, },
); );
@ -576,7 +580,7 @@ export function useSubmitThread({
}, },
context: { context: {
...threadContext, ...threadContext,
thread_id: threadId, ...(threadId ? { thread_id: threadId } : {}),
}, },
}, },
); );

View File

@ -156,7 +156,6 @@ export async function downloadMarkdownAsPdf(
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
async function loadHtml2Pdf(): Promise<Function> { async function loadHtml2Pdf(): Promise<Function> {
const html2pdf = await import("html2pdf.js"); const html2pdf = await import("html2pdf.js");
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
return html2pdf.default; return html2pdf.default;
} }

View File

@ -25,7 +25,7 @@ interface SkillError {
interface UseSelectedSkillListenerOptions { interface UseSelectedSkillListenerOptions {
/** 当前会话 thread_id用于调用 bootstrapRemoteSkill */ /** 当前会话 thread_id用于调用 bootstrapRemoteSkill */
threadId: string | null; threadId?: string | null;
} }
interface UseSelectedSkillListenerReturn { interface UseSelectedSkillListenerReturn {