feat: add download ZIP and push to GitHub features, fix code overflow in chat

This commit is contained in:
Gemini AI
2025-12-29 19:20:25 +04:00
Unverified
parent 5fcc6c0948
commit 3d00e5dca4
6 changed files with 386 additions and 3 deletions

View File

@@ -9,6 +9,8 @@ import {
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeHighlight from "rehype-highlight";
import { Download, Github } from "lucide-react";
import { downloadArtifactAsZip, pushToGithub } from "@/lib/artifact-utils";
import { cn } from "@/lib/utils";
import { AIAssistMessage } from "@/types";
import { Button } from "@/components/ui/button";
@@ -443,7 +445,9 @@ export default function AIAssist() {
selectedProvider,
selectedModels,
setSelectedModel,
setSelectedProvider
setSelectedProvider,
githubToken,
setGithubToken
} = useStore();
const t = translations[language].aiAssist;
const common = translations[language].common;
@@ -473,6 +477,11 @@ export default function AIAssist() {
const [isAuthenticatingQwen, setIsAuthenticatingQwen] = useState(false);
const [qwenAuthError, setQwenAuthError] = useState<string | null>(null);
const [isPushingToGithub, setIsPushingToGithub] = useState(false);
const [showGithubDialog, setShowGithubDialog] = useState(false);
const [githubRepoName, setGithubRepoName] = useState("my-ai-artifact");
const [tempGithubToken, setTempGithubToken] = useState(githubToken || "");
// Check if Qwen is authenticated
const isQwenAuthed = modelAdapter.hasQwenAuth();
@@ -715,6 +724,31 @@ export default function AIAssist() {
}
};
const handleDownloadZip = () => {
if (!previewData) return;
downloadArtifactAsZip(previewData.data, previewData.type, previewData.language);
};
const handlePushToGithub = async () => {
if (!previewData || !tempGithubToken || !githubRepoName) return;
setIsPushingToGithub(true);
try {
setGithubToken(tempGithubToken);
const extension = previewData.language === "html" || previewData.type === "web" || previewData.type === "app" ? "html" : (previewData.language === "typescript" || previewData.language === "tsx" ? "tsx" : "txt");
const files = [
{ path: `index.${extension}`, content: previewData.data },
{ path: "README.md", content: `# Generated by PromptArch\n\nType: ${previewData.type}\nGenerated: ${new Date().toLocaleString()}` }
];
const result = await pushToGithub(tempGithubToken, githubRepoName, files);
alert(`${t.successPush}\nURL: ${result.url}`);
setShowGithubDialog(false);
} catch (err) {
alert(`${t.errorPush}: ${err instanceof Error ? err.message : "Unknown error"}`);
} finally {
setIsPushingToGithub(false);
}
};
return (
<div className="ai-assist h-[calc(100vh-140px)] flex flex-col lg:flex-row gap-4 lg:gap-8 overflow-hidden animate-in fade-in duration-700">
{/* --- Chat Panel --- */}
@@ -934,8 +968,32 @@ export default function AIAssist() {
</button>
</div>
<div className="prose prose-sm dark:prose-invert max-w-none leading-relaxed font-medium">
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeHighlight]}>
<div className="prose prose-sm dark:prose-invert max-w-full overflow-hidden leading-relaxed font-medium">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeHighlight]}
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || "");
return !inline ? (
<div className="relative group/code w-full overflow-hidden">
<pre className={cn(
"p-4 rounded-xl bg-slate-950/50 border border-white/5 overflow-x-auto scrollbar-thin scrollbar-thumb-white/10",
className
)}>
<code className={cn("whitespace-pre", className)} {...props}>
{children}
</code>
</pre>
</div>
) : (
<code className={cn("bg-blue-500/10 dark:bg-blue-500/20 px-1.5 py-0.5 rounded text-blue-600 dark:text-blue-300", className)} {...props}>
{children}
</code>
);
},
}}
>
{parseStreamingContent(msg.content, msg.agent || "general").chatDisplay || (msg.role === "assistant" ? "..." : "")}
</ReactMarkdown>
</div>
@@ -1103,6 +1161,24 @@ export default function AIAssist() {
>
<Copy className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-10 w-10 text-blue-200/70 hover:text-white hover:bg-blue-900 rounded-2xl"
onClick={handleDownloadZip}
title={t.downloadZip}
>
<Download className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-10 w-10 text-blue-200/70 hover:text-white hover:bg-blue-900 rounded-2xl"
onClick={() => setShowGithubDialog(true)}
title={t.pushToGithub}
>
<Github className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
@@ -1159,6 +1235,65 @@ export default function AIAssist() {
color: inherit;
}
`}</style>
{/* GitHub Push Dialog */}
{showGithubDialog && (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-md p-4 animate-in fade-in duration-300">
<Card className="w-full max-w-md bg-[#0b1414] border border-blue-900/50 shadow-2xl p-8 rounded-[2rem] animate-in zoom-in-95 duration-300">
<div className="flex items-center gap-4 mb-8">
<div className="p-3 bg-white rounded-2xl">
<Github className="h-6 w-6 text-black" />
</div>
<div>
<h3 className="text-xl font-black text-white">{t.pushToGithub}</h3>
<p className="text-xs text-blue-200/50 mt-1">{t.githubAuth}</p>
</div>
<Button
variant="ghost"
size="icon"
className="ml-auto text-blue-200/40 hover:text-white"
onClick={() => setShowGithubDialog(false)}
>
<X className="h-5 w-5" />
</Button>
</div>
<div className="space-y-6">
<div className="space-y-2">
<label className="text-[10px] font-black uppercase tracking-widest text-blue-400">{t.enterGithubToken}</label>
<Input
type="password"
value={tempGithubToken}
onChange={(e) => setTempGithubToken(e.target.value)}
placeholder="ghp_xxxxxxxxxxxx"
className="bg-white/5 border-blue-900/50 text-white placeholder:text-blue-900/50 rounded-xl h-12"
/>
<p className="text-[9px] text-blue-200/30">
Requires 'repo' scope. Get one at github.com/settings/tokens
</p>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black uppercase tracking-widest text-blue-400">{t.repositoryName}</label>
<Input
value={githubRepoName}
onChange={(e) => setGithubRepoName(e.target.value)}
placeholder="repo-name"
className="bg-white/5 border-blue-900/50 text-white placeholder:text-blue-900/50 rounded-xl h-12"
/>
</div>
<Button
onClick={handlePushToGithub}
disabled={isPushingToGithub || !tempGithubToken || !githubRepoName}
className="w-full bg-blue-600 hover:bg-blue-500 text-white font-black uppercase tracking-widest py-6 rounded-2xl shadow-lg shadow-blue-500/20 disabled:opacity-50"
>
{isPushingToGithub ? t.pushingToGithub : t.pushToGithub}
</Button>
</div>
</Card>
</div>
)}
</div >
);
}