Compare commits

...

4 Commits

5 changed files with 118 additions and 26 deletions

View File

@ -333,8 +333,7 @@ export default function ChatPage() {
)}
>
<div className="flex size-full justify-center">
{!showWelcomeStyle &&
<MessageList
<MessageList
className={cn(
"size-full",
(!showWelcomeStyle || hasSubmitted) && "pt-[58px]",
@ -347,8 +346,10 @@ export default function ChatPage() {
: thread.messages.slice(historyCutoff)
}
paddingBottom={todoListCollapsed ? 160 : 280}
// !showWelcomeStyle || hasSubmitted
showScrollToBottomButton={!showWelcomeStyle}
scrollButtonClassName="bottom-[112px]"
/>
}
</div>
</main>
</div>

View File

@ -84,7 +84,7 @@ export const ConversationScrollButton = ({
!isAtBottom && (
<Button
className={cn(
"absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full",
"absolute bottom-3 left-1/2 -translate-x-1/2 rounded-full",
className,
)}
onClick={handleScrollToBottom}

View File

@ -9,6 +9,7 @@ import {
useEffect,
useMemo,
useState,
type ComponentProps,
type HTMLAttributes,
} from "react";
import { toast } from "sonner";
@ -32,7 +33,7 @@ import { DropdownSelector } from "@/components/ui/dropdown-selector";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { CodeEditor } from "@/components/workspace/code-editor";
import { useArtifactContent } from "@/core/artifacts/hooks";
import { urlOfArtifact } from "@/core/artifacts/utils";
import { resolveArtifactURL, urlOfArtifact } from "@/core/artifacts/utils";
import { useI18n } from "@/core/i18n/hooks";
import { streamdownPlugins } from "@/core/streamdown";
import { checkCodeFile, getFileName } from "@/core/utils/files";
@ -473,11 +474,12 @@ export function ArtifactFileDetail({
content={displayContent}
language={language ?? "text"}
zoom={zoom}
threadId={threadId}
/>
)}
{isCodeFile && viewMode === "code" && (
<div className="min-h-full mb-[180px] rounded-b-[10px] bg-white p-0 mb-0">
<div className="min-h-full mb-[207px] rounded-b-[10px] bg-white p-0 mb-0">
<CodeEditor
className="size-full resize-none rounded-none border-none py-[20px]"
value={displayContent ?? ""}
@ -487,14 +489,13 @@ export function ArtifactFileDetail({
</div>
)}
{!isCodeFile && (
<div className="h-full mb-[180px]">
<iframe
className="size-full border-0"
srcDoc={artifactViewerSrcDoc}
sandbox="allow-same-origin allow-scripts allow-downloads"
title={`Artifact preview: ${fileName}`}
/>
</div>
<PreviewIframe
className="size-full border-0"
containerClassName="h-full mb-[207px]"
srcDoc={artifactViewerSrcDoc}
sandbox="allow-same-origin allow-scripts allow-downloads"
title={`Artifact preview: ${fileName}`}
/>
)}
</ArtifactContent>
</Artifact>
@ -505,17 +506,22 @@ export function ArtifactFilePreview({
content,
language,
zoom = 100,
threadId,
}: {
content: string;
language: string;
zoom?: number;
threadId?: string;
}) {
const zoomScale = zoom / 100;
const normalizedContent = useMemo(() => {
return rewriteArtifactImagePaths(content ?? "", threadId);
}, [content, threadId]);
if (language === "markdown") {
return (
<div
className={cn("w-full bg-white mb-[207px] p-[20px]")}
className={cn("w-full bg-white mb-[207px] p-[20px]")}
style={{ "--zoom-scale": zoomScale } as React.CSSProperties}
>
<Streamdown
@ -523,7 +529,7 @@ export function ArtifactFilePreview({
{...streamdownPlugins}
components={{ a: CitationLink }}
>
{content ?? ""}
{normalizedContent}
</Streamdown>
</div>
@ -531,21 +537,81 @@ export function ArtifactFilePreview({
}
if (language === "html") {
return (
<div className="h-full mb-[180px]">
<iframe
className="size-full"
title="Artifact preview"
srcDoc={content}
sandbox="allow-scripts allow-forms"
style={{ zoom: zoomScale }}
/>
</div>
<PreviewIframe
className="size-full"
containerClassName="h-full mb-[207px]"
title="Artifact preview"
srcDoc={normalizedContent}
sandbox="allow-scripts allow-forms"
style={{ zoom: zoomScale }}
/>
);
}
return null;
}
function PreviewIframe({
className,
containerClassName,
onLoad,
src,
srcDoc,
...props
}: ComponentProps<"iframe"> & {
containerClassName?: string;
}) {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setIsLoading(true);
}, [src, srcDoc]);
return (
<div className={cn("relative", containerClassName)}>
<iframe
className={className}
src={src}
srcDoc={srcDoc}
onLoad={(event) => {
setIsLoading(false);
onLoad?.(event);
}}
{...props}
/>
{isLoading && (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/85">
<LoaderIcon className="text-muted-foreground size-5 animate-spin" />
</div>
)}
</div>
);
}
function rewriteArtifactImagePaths(content: string, threadId?: string) {
if (!threadId || !/\/?mnt\/user-data\//.test(content)) {
return content;
}
const markdownRewritten = content.replace(
/!\[([^\]]*)\]\(\s*(\/?mnt\/user-data\/outputs\/[^)\s]+)\s*\)/g,
(_full, alt, rawPath) => {
const normalizedPath = rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
const artifactUrl = resolveArtifactURL(normalizedPath, threadId);
return `![${alt}](${artifactUrl})`;
},
);
return markdownRewritten.replace(
/(<img\b[^>]*\bsrc\s*=\s*)(["'])(\/?mnt\/user-data\/outputs\/[^"']+)\2/gi,
(_full, prefix, quote, rawPath) => {
const normalizedPath = rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
const artifactUrl = resolveArtifactURL(normalizedPath, threadId);
return `${prefix}${quote}${artifactUrl}${quote}`;
},
);
}
type ArtifactPreviewKind =
| "html"
| "image"

View File

@ -4,6 +4,7 @@ import type { UseStream } from "@langchain/langgraph-sdk/react";
import {
Conversation,
ConversationContent,
ConversationScrollButton,
} from "@/components/ai-elements/conversation";
import { useI18n } from "@/core/i18n/hooks";
import {
@ -37,6 +38,8 @@ export function MessageList({
messagesOverride,
suppressThreadLoading = false,
paddingBottom = 160,
showScrollToBottomButton = false,
scrollButtonClassName,
}: {
className?: string;
threadId?: string;
@ -45,6 +48,8 @@ export function MessageList({
messagesOverride?: Message[];
suppressThreadLoading?: boolean;
paddingBottom?: number;
showScrollToBottomButton?: boolean;
scrollButtonClassName?: string;
}) {
const { t } = useI18n();
const rehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading);
@ -205,9 +210,19 @@ export function MessageList({
/>
);
})}
{thread.isLoading && <StreamingIndicator className="my-4" />}
{thread.isLoading && messages.length > 0 && <StreamingIndicator className="my-4" />}
<div style={{ height: `${paddingBottom}px` }} />
</ConversationContent>
{/* showScrollToBottomButton */}
{ showScrollToBottomButton && (
<ConversationScrollButton
className={cn(
"z-20 rounded-full border bg-white/90 shadow-sm backdrop-blur-sm",
scrollButtonClassName,
)}
title="滚动到底部"
/>
)}
</Conversation>
);
}

View File

@ -12,6 +12,16 @@ The user provides frontend requirements: a component, page, application, or inte
**MANDATORY**: The entry HTML file MUST be named `index.html`. This is a strict requirement for all generated frontend projects to ensure compatibility with standard web hosting and deployment workflows.
### CDN Requirement (China-Friendly)
When generating plain HTML projects that reference third-party JS/CSS from CDN, prefer China-friendly CDN links by default:
1. `https://cdn.bootcdn.net` (preferred)
2. `https://cdn.staticfile.net` (fallback)
3. `https://registry.npmmirror.com` (for npm package file URLs when needed)
Avoid `unpkg.com` and `cdnjs.cloudflare.com` as primary CDN links in generated HTML.
## Design Thinking
Before coding, understand the context and commit to a BOLD aesthetic direction: