Compare commits
No commits in common. "2f9e6824454d0a6f93b4afda8fb87de999c4e9fb" and "1c3f31ac0b274badf73cdf058fcc447bbe2a118d" have entirely different histories.
2f9e682445
...
1c3f31ac0b
|
|
@ -333,7 +333,8 @@ export default function ChatPage() {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex size-full justify-center">
|
<div className="flex size-full justify-center">
|
||||||
<MessageList
|
{!showWelcomeStyle &&
|
||||||
|
<MessageList
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-full",
|
"size-full",
|
||||||
(!showWelcomeStyle || hasSubmitted) && "pt-[58px]",
|
(!showWelcomeStyle || hasSubmitted) && "pt-[58px]",
|
||||||
|
|
@ -346,10 +347,8 @@ export default function ChatPage() {
|
||||||
: thread.messages.slice(historyCutoff)
|
: thread.messages.slice(historyCutoff)
|
||||||
}
|
}
|
||||||
paddingBottom={todoListCollapsed ? 160 : 280}
|
paddingBottom={todoListCollapsed ? 160 : 280}
|
||||||
// !showWelcomeStyle || hasSubmitted
|
|
||||||
showScrollToBottomButton={!showWelcomeStyle}
|
|
||||||
scrollButtonClassName="bottom-[112px]"
|
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ export const ConversationScrollButton = ({
|
||||||
!isAtBottom && (
|
!isAtBottom && (
|
||||||
<Button
|
<Button
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute bottom-3 left-1/2 -translate-x-1/2 rounded-full",
|
"absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
onClick={handleScrollToBottom}
|
onClick={handleScrollToBottom}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import {
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
type ComponentProps,
|
|
||||||
type HTMLAttributes,
|
type HTMLAttributes,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
@ -33,7 +32,7 @@ import { DropdownSelector } from "@/components/ui/dropdown-selector";
|
||||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||||
import { CodeEditor } from "@/components/workspace/code-editor";
|
import { CodeEditor } from "@/components/workspace/code-editor";
|
||||||
import { useArtifactContent } from "@/core/artifacts/hooks";
|
import { useArtifactContent } from "@/core/artifacts/hooks";
|
||||||
import { resolveArtifactURL, urlOfArtifact } from "@/core/artifacts/utils";
|
import { urlOfArtifact } from "@/core/artifacts/utils";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { streamdownPlugins } from "@/core/streamdown";
|
import { streamdownPlugins } from "@/core/streamdown";
|
||||||
import { checkCodeFile, getFileName } from "@/core/utils/files";
|
import { checkCodeFile, getFileName } from "@/core/utils/files";
|
||||||
|
|
@ -474,12 +473,11 @@ export function ArtifactFileDetail({
|
||||||
content={displayContent}
|
content={displayContent}
|
||||||
language={language ?? "text"}
|
language={language ?? "text"}
|
||||||
zoom={zoom}
|
zoom={zoom}
|
||||||
threadId={threadId}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
{isCodeFile && viewMode === "code" && (
|
{isCodeFile && viewMode === "code" && (
|
||||||
<div className="min-h-full mb-[207px] rounded-b-[10px] bg-white p-0 mb-0">
|
<div className="min-h-full mb-[180px] 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 ?? ""}
|
||||||
|
|
@ -489,13 +487,14 @@ export function ArtifactFileDetail({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isCodeFile && (
|
{!isCodeFile && (
|
||||||
<PreviewIframe
|
<div className="h-full mb-[180px]">
|
||||||
className="size-full border-0"
|
<iframe
|
||||||
containerClassName="h-full mb-[207px]"
|
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>
|
||||||
|
|
@ -506,22 +505,17 @@ export function ArtifactFilePreview({
|
||||||
content,
|
content,
|
||||||
language,
|
language,
|
||||||
zoom = 100,
|
zoom = 100,
|
||||||
threadId,
|
|
||||||
}: {
|
}: {
|
||||||
content: string;
|
content: string;
|
||||||
language: string;
|
language: string;
|
||||||
zoom?: number;
|
zoom?: number;
|
||||||
threadId?: string;
|
|
||||||
}) {
|
}) {
|
||||||
const zoomScale = zoom / 100;
|
const zoomScale = zoom / 100;
|
||||||
const normalizedContent = useMemo(() => {
|
|
||||||
return rewriteArtifactImagePaths(content ?? "", threadId);
|
|
||||||
}, [content, threadId]);
|
|
||||||
|
|
||||||
if (language === "markdown") {
|
if (language === "markdown") {
|
||||||
return (
|
return (
|
||||||
<div
|
<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}
|
style={{ "--zoom-scale": zoomScale } as React.CSSProperties}
|
||||||
>
|
>
|
||||||
<Streamdown
|
<Streamdown
|
||||||
|
|
@ -529,7 +523,7 @@ export function ArtifactFilePreview({
|
||||||
{...streamdownPlugins}
|
{...streamdownPlugins}
|
||||||
components={{ a: CitationLink }}
|
components={{ a: CitationLink }}
|
||||||
>
|
>
|
||||||
{normalizedContent}
|
{content ?? ""}
|
||||||
</Streamdown>
|
</Streamdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -537,81 +531,21 @@ export function ArtifactFilePreview({
|
||||||
}
|
}
|
||||||
if (language === "html") {
|
if (language === "html") {
|
||||||
return (
|
return (
|
||||||
<PreviewIframe
|
<div className="h-full mb-[180px]">
|
||||||
className="size-full"
|
<iframe
|
||||||
containerClassName="h-full mb-[207px]"
|
className="size-full"
|
||||||
title="Artifact preview"
|
title="Artifact preview"
|
||||||
srcDoc={normalizedContent}
|
srcDoc={content}
|
||||||
sandbox="allow-scripts allow-forms"
|
sandbox="allow-scripts allow-forms"
|
||||||
style={{ zoom: zoomScale }}
|
style={{ zoom: zoomScale }}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
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 ``;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
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 =
|
type ArtifactPreviewKind =
|
||||||
| "html"
|
| "html"
|
||||||
| "image"
|
| "image"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import type { UseStream } from "@langchain/langgraph-sdk/react";
|
||||||
import {
|
import {
|
||||||
Conversation,
|
Conversation,
|
||||||
ConversationContent,
|
ConversationContent,
|
||||||
ConversationScrollButton,
|
|
||||||
} from "@/components/ai-elements/conversation";
|
} from "@/components/ai-elements/conversation";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import {
|
import {
|
||||||
|
|
@ -38,8 +37,6 @@ export function MessageList({
|
||||||
messagesOverride,
|
messagesOverride,
|
||||||
suppressThreadLoading = false,
|
suppressThreadLoading = false,
|
||||||
paddingBottom = 160,
|
paddingBottom = 160,
|
||||||
showScrollToBottomButton = false,
|
|
||||||
scrollButtonClassName,
|
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
threadId?: string;
|
threadId?: string;
|
||||||
|
|
@ -48,8 +45,6 @@ export function MessageList({
|
||||||
messagesOverride?: Message[];
|
messagesOverride?: Message[];
|
||||||
suppressThreadLoading?: boolean;
|
suppressThreadLoading?: boolean;
|
||||||
paddingBottom?: number;
|
paddingBottom?: number;
|
||||||
showScrollToBottomButton?: boolean;
|
|
||||||
scrollButtonClassName?: string;
|
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const rehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading);
|
const rehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading);
|
||||||
|
|
@ -210,19 +205,9 @@ export function MessageList({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{thread.isLoading && messages.length > 0 && <StreamingIndicator className="my-4" />}
|
{thread.isLoading && <StreamingIndicator className="my-4" />}
|
||||||
<div style={{ height: `${paddingBottom}px` }} />
|
<div style={{ height: `${paddingBottom}px` }} />
|
||||||
</ConversationContent>
|
</ConversationContent>
|
||||||
{/* showScrollToBottomButton */}
|
|
||||||
{ showScrollToBottomButton && (
|
|
||||||
<ConversationScrollButton
|
|
||||||
className={cn(
|
|
||||||
"z-20 rounded-full border bg-white/90 shadow-sm backdrop-blur-sm",
|
|
||||||
scrollButtonClassName,
|
|
||||||
)}
|
|
||||||
title="滚动到底部"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Conversation>
|
</Conversation>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,6 @@ 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.
|
**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
|
## Design Thinking
|
||||||
|
|
||||||
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue