enzostvs HF Staff commited on
Commit
9c17740
·
1 Parent(s): c2bb873

add live preview

Browse files
app/api/me/projects/route.ts CHANGED
@@ -1,5 +1,5 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { RepoDesignation, createRepo, listCommits, uploadFiles } from "@huggingface/hub";
3
 
4
  import { isAuthenticated } from "@/lib/auth";
5
  import { Commit, Page } from "@/types";
@@ -55,15 +55,16 @@ This project was created with [DeepSite](https://deepsite.hf.co).
55
  });
56
 
57
  try {
58
- const { repoUrl } = await createRepo({
59
  repo,
60
  accessToken: user.token as string,
61
  });
 
62
  await uploadFiles({
63
  repo,
64
  files,
65
  accessToken: user.token as string,
66
- commitTitle: prompt ?? "Redesign my website"
67
  });
68
 
69
  const path = repoUrl.split("/").slice(-2).join("/");
@@ -80,12 +81,19 @@ This project was created with [DeepSite](https://deepsite.hf.co).
80
  });
81
  }
82
 
 
 
 
 
 
83
  let newProject = {
84
  files,
85
  pages,
86
  commits,
87
  project: {
88
-
 
 
89
  }
90
  }
91
 
 
1
  import { NextRequest, NextResponse } from "next/server";
2
+ import { RepoDesignation, createRepo, listCommits, spaceInfo, uploadFiles } from "@huggingface/hub";
3
 
4
  import { isAuthenticated } from "@/lib/auth";
5
  import { Commit, Page } from "@/types";
 
55
  });
56
 
57
  try {
58
+ const { repoUrl} = await createRepo({
59
  repo,
60
  accessToken: user.token as string,
61
  });
62
+ const commitTitle = !prompt || prompt.trim() === "" ? "Redesign my website" : prompt;
63
  await uploadFiles({
64
  repo,
65
  files,
66
  accessToken: user.token as string,
67
+ commitTitle
68
  });
69
 
70
  const path = repoUrl.split("/").slice(-2).join("/");
 
81
  });
82
  }
83
 
84
+ const space = await spaceInfo({
85
+ name: repo.name,
86
+ accessToken: user.token as string,
87
+ });
88
+
89
  let newProject = {
90
  files,
91
  pages,
92
  commits,
93
  project: {
94
+ id: space.id,
95
+ space_id: space.name,
96
+ _updatedAt: space.updatedAt,
97
  }
98
  }
99
 
components/editor/ask-ai/loading.tsx CHANGED
@@ -1,18 +1,45 @@
1
  import Loading from "@/components/loading";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  export const AiLoading = ({
4
- text = "Ai is working...",
5
  className,
6
  }: {
7
  text?: string;
8
  className?: string;
9
  }) => {
 
 
 
 
 
 
 
 
 
 
 
 
10
  return (
11
  <div className={`flex items-center justify-start gap-2 ${className}`}>
12
- <Loading overlay={false} className="!size-4 opacity-50" />
13
  <p className="text-neutral-400 text-sm">
14
  <span className="inline-flex">
15
- {text.split("").map((char, index) => (
16
  <span
17
  key={index}
18
  className="bg-gradient-to-r from-neutral-100 to-neutral-300 bg-clip-text text-transparent animate-pulse"
 
1
  import Loading from "@/components/loading";
2
+ import { useState } from "react";
3
+ import { useInterval } from "react-use";
4
+
5
+ const TEXTS = [
6
+ "Teaching pixels to dance with style...",
7
+ "AI is having a creative breakthrough...",
8
+ "Channeling digital vibes into pure code...",
9
+ "Summoning the website spirits...",
10
+ "Brewing some algorithmic magic...",
11
+ "Composing a symphony of divs and spans...",
12
+ "Riding the wave of computational creativity...",
13
+ "Aligning the stars for perfect design...",
14
+ "Training circus animals to write CSS...",
15
+ "Launching ideas into the digital stratosphere...",
16
+ ];
17
 
18
  export const AiLoading = ({
19
+ text,
20
  className,
21
  }: {
22
  text?: string;
23
  className?: string;
24
  }) => {
25
+ const [selectedText, setSelectedText] = useState(
26
+ text ?? TEXTS[Math.floor(Math.random() * TEXTS.length)]
27
+ );
28
+ useInterval(() => {
29
+ if (!text) {
30
+ if (selectedText === TEXTS[TEXTS.length - 1]) {
31
+ setSelectedText(TEXTS[0]);
32
+ } else {
33
+ setSelectedText(TEXTS[TEXTS.indexOf(selectedText) + 1]);
34
+ }
35
+ }
36
+ }, 12000);
37
  return (
38
  <div className={`flex items-center justify-start gap-2 ${className}`}>
39
+ <Loading overlay={false} className="!size-5 opacity-50" />
40
  <p className="text-neutral-400 text-sm">
41
  <span className="inline-flex">
42
+ {selectedText.split("").map((char, index) => (
43
  <span
44
  key={index}
45
  className="bg-gradient-to-r from-neutral-100 to-neutral-300 bg-clip-text text-transparent animate-pulse"
components/editor/live-preview/index.tsx ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useEffect, useRef } from "react";
4
+ import classNames from "classnames";
5
+
6
+ import { Button } from "@/components/ui/button";
7
+ import { Maximize, Minimize } from "lucide-react";
8
+
9
+ interface LivePreviewProps {
10
+ currentPageData: { path: string; html: string } | undefined;
11
+ isAiWorking: boolean;
12
+ defaultHTML: string;
13
+ className?: string;
14
+ }
15
+
16
+ export const LivePreview = ({
17
+ currentPageData,
18
+ isAiWorking,
19
+ defaultHTML,
20
+ className,
21
+ }: LivePreviewProps) => {
22
+ const [isMaximized, setIsMaximized] = useState(false);
23
+ const [displayedHtml, setDisplayedHtml] = useState<string>("");
24
+ const latestHtmlRef = useRef<string>("");
25
+ const displayedHtmlRef = useRef<string>("");
26
+ const intervalRef = useRef<NodeJS.Timeout | null>(null);
27
+
28
+ useEffect(() => {
29
+ displayedHtmlRef.current = displayedHtml;
30
+ }, [displayedHtml]);
31
+
32
+ useEffect(() => {
33
+ if (currentPageData?.html && currentPageData.html !== defaultHTML) {
34
+ latestHtmlRef.current = currentPageData.html;
35
+ }
36
+ }, [currentPageData?.html, defaultHTML]);
37
+
38
+ useEffect(() => {
39
+ if (!currentPageData?.html || currentPageData.html === defaultHTML) {
40
+ return;
41
+ }
42
+
43
+ if (!displayedHtml || !isAiWorking) {
44
+ setDisplayedHtml(currentPageData.html);
45
+ if (intervalRef.current) {
46
+ clearInterval(intervalRef.current);
47
+ intervalRef.current = null;
48
+ }
49
+ return;
50
+ }
51
+
52
+ if (isAiWorking && !intervalRef.current) {
53
+ intervalRef.current = setInterval(() => {
54
+ if (
55
+ latestHtmlRef.current &&
56
+ latestHtmlRef.current !== displayedHtmlRef.current
57
+ ) {
58
+ setDisplayedHtml(latestHtmlRef.current);
59
+ }
60
+ }, 3000);
61
+ }
62
+ }, [currentPageData?.html, defaultHTML, isAiWorking, displayedHtml]);
63
+
64
+ useEffect(() => {
65
+ if (!isAiWorking && intervalRef.current) {
66
+ clearInterval(intervalRef.current);
67
+ intervalRef.current = null;
68
+ if (latestHtmlRef.current) {
69
+ setDisplayedHtml(latestHtmlRef.current);
70
+ }
71
+ }
72
+ }, [isAiWorking]);
73
+
74
+ useEffect(() => {
75
+ return () => {
76
+ if (intervalRef.current) {
77
+ clearInterval(intervalRef.current);
78
+ intervalRef.current = null;
79
+ }
80
+ };
81
+ }, []);
82
+
83
+ if (!displayedHtml) {
84
+ return null;
85
+ }
86
+
87
+ return (
88
+ <div
89
+ className={classNames(
90
+ "absolute z-40 bg-white/95 backdrop-blur-sm border border-neutral-200 shadow-lg transition-all duration-500 ease-out transform scale-100 opacity-100 animate-in slide-in-from-bottom-4 zoom-in-95 rounded-xl",
91
+ {
92
+ "shadow-green-500/20 shadow-2xl border-green-200": isAiWorking,
93
+ },
94
+ className
95
+ )}
96
+ >
97
+ <div
98
+ className={classNames(
99
+ "flex flex-col animate-in fade-in duration-300",
100
+ isMaximized ? "w-[60dvw] h-[80dvh]" : "w-80 h-96"
101
+ )}
102
+ >
103
+ <div className="flex items-center justify-between p-3 border-b border-neutral-200">
104
+ <div className="flex items-center gap-2">
105
+ <div className="size-2 bg-green-500 rounded-full animate-pulse shadow-sm shadow-green-500/50"></div>
106
+ <span className="text-xs font-medium text-neutral-800">
107
+ Live Preview
108
+ </span>
109
+ {isAiWorking && (
110
+ <span className="text-xs text-green-600 font-medium animate-pulse">
111
+ • Updating
112
+ </span>
113
+ )}
114
+ </div>
115
+ <div className="flex items-center gap-1">
116
+ <Button
117
+ variant="outline"
118
+ size="iconXs"
119
+ className="!rounded-md !border-neutral-200 hover:bg-neutral-50"
120
+ onClick={() => setIsMaximized(!isMaximized)}
121
+ >
122
+ {isMaximized ? (
123
+ <Minimize className="text-neutral-400 size-3" />
124
+ ) : (
125
+ <Maximize className="text-neutral-400 size-3" />
126
+ )}
127
+ </Button>
128
+ </div>
129
+ </div>
130
+ <div className="flex-1 bg-black overflow-hidden relative rounded-b-xl">
131
+ <iframe
132
+ className="w-full h-full border-0"
133
+ srcDoc={displayedHtml}
134
+ sandbox="allow-scripts allow-same-origin"
135
+ title="Live Preview"
136
+ />
137
+ </div>
138
+ </div>
139
+ </div>
140
+ );
141
+ };
components/editor/preview/index.tsx CHANGED
@@ -13,6 +13,7 @@ import { AnimatedBlobs } from "@/components/animated-blobs";
13
  import { AiLoading } from "../ask-ai/loading";
14
  import { defaultHTML } from "@/lib/consts";
15
  import { Button } from "@/components/ui/button";
 
16
  import {
17
  MousePointerClick,
18
  History,
@@ -32,6 +33,7 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
32
  currentTab,
33
  currentCommit,
34
  setCurrentCommit,
 
35
  } = useEditor();
36
  const {
37
  isEditableModeEnabled,
@@ -195,12 +197,12 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
195
  ) : iframeSrc === "" ||
196
  isLoadingProject ||
197
  (isAiWorking && iframeSrc == "") ? (
198
- <div className="w-full h-full flex items-center justify-center">
199
  <div className="py-10 w-full relative z-1 max-w-3xl mx-auto text-center">
200
  <AiLoading
201
  text={
202
  isAiWorking && iframeSrc === ""
203
- ? "Creating your Project..."
204
  : "Fetching your space..."
205
  }
206
  className="flex-col"
@@ -208,6 +210,12 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
208
  <AnimatedBlobs />
209
  <AnimatedBlobs />
210
  </div>
 
 
 
 
 
 
211
  </div>
212
  ) : (
213
  <>
 
13
  import { AiLoading } from "../ask-ai/loading";
14
  import { defaultHTML } from "@/lib/consts";
15
  import { Button } from "@/components/ui/button";
16
+ import { LivePreview } from "../live-preview";
17
  import {
18
  MousePointerClick,
19
  History,
 
33
  currentTab,
34
  currentCommit,
35
  setCurrentCommit,
36
+ currentPageData,
37
  } = useEditor();
38
  const {
39
  isEditableModeEnabled,
 
197
  ) : iframeSrc === "" ||
198
  isLoadingProject ||
199
  (isAiWorking && iframeSrc == "") ? (
200
+ <div className="w-full h-full flex items-center justify-center relative">
201
  <div className="py-10 w-full relative z-1 max-w-3xl mx-auto text-center">
202
  <AiLoading
203
  text={
204
  isAiWorking && iframeSrc === ""
205
+ ? undefined
206
  : "Fetching your space..."
207
  }
208
  className="flex-col"
 
210
  <AnimatedBlobs />
211
  <AnimatedBlobs />
212
  </div>
213
+ <LivePreview
214
+ currentPageData={currentPageData}
215
+ isAiWorking={isAiWorking}
216
+ defaultHTML={defaultHTML}
217
+ className="bottom-4 left-4"
218
+ />
219
  </div>
220
  ) : (
221
  <>
lib/prompts.ts CHANGED
@@ -18,7 +18,7 @@ export const PROMPT_FOR_REWRITE_PROMPT_END = " >>>>>>> PROMPT_FOR_REWRITE_PROMPT
18
 
19
  export const PROMPT_FOR_IMAGE_GENERATION = `If you want to use image placeholder, http://Static.photos Usage:Format: http://static.photos/[category]/[dimensions]/[seed] where dimensions must be one of: 200x200, 320x240, 640x360, 1024x576, or 1200x630; seed can be any number (1-999+) for consistent images or omit for random; categories include: nature, office, people, technology, minimal, abstract, aerial, blurred, bokeh, gradient, monochrome, vintage, white, black, blue, red, green, yellow, cityscape, workspace, food, travel, textures, industry, indoor, outdoor, studio, finance, medical, season, holiday, event, sport, science, legal, estate, restaurant, retail, wellness, agriculture, construction, craft, cosmetic, automotive, gaming, or education.
20
  Examples: http://static.photos/red/320x240/133 (red-themed with seed 133), http://static.photos/640x360 (random category and image), http://static.photos/nature/1200x630/42 (nature-themed with seed 42).`
21
- export const PROMPT_FOR_PROJECT_NAME = `REQUIRED: Generate a name for the project, based on the user's request. Try to be creative and unique. It should be short and concise (max 6 words). DON'T FORGET IT, IT'S IMPORTANT!`
22
 
23
  export const INITIAL_SYSTEM_PROMPT = `You are an expert UI/UX and Front-End Developer.
24
  You create website in a way a designer would, using ONLY HTML, CSS and Javascript.
 
18
 
19
  export const PROMPT_FOR_IMAGE_GENERATION = `If you want to use image placeholder, http://Static.photos Usage:Format: http://static.photos/[category]/[dimensions]/[seed] where dimensions must be one of: 200x200, 320x240, 640x360, 1024x576, or 1200x630; seed can be any number (1-999+) for consistent images or omit for random; categories include: nature, office, people, technology, minimal, abstract, aerial, blurred, bokeh, gradient, monochrome, vintage, white, black, blue, red, green, yellow, cityscape, workspace, food, travel, textures, industry, indoor, outdoor, studio, finance, medical, season, holiday, event, sport, science, legal, estate, restaurant, retail, wellness, agriculture, construction, craft, cosmetic, automotive, gaming, or education.
20
  Examples: http://static.photos/red/320x240/133 (red-themed with seed 133), http://static.photos/640x360 (random category and image), http://static.photos/nature/1200x630/42 (nature-themed with seed 42).`
21
+ export const PROMPT_FOR_PROJECT_NAME = `REQUIRED: Generate a name for the project, based on the user's request. Try to be creative and unique. Add a emoji at the end of the name. It should be short, like 6 words. Be fancy, creative and funny. DON'T FORGET IT, IT'S IMPORTANT!`
22
 
23
  export const INITIAL_SYSTEM_PROMPT = `You are an expert UI/UX and Front-End Developer.
24
  You create website in a way a designer would, using ONLY HTML, CSS and Javascript.
lib/providers.ts CHANGED
@@ -86,6 +86,7 @@ export const MODELS = [
86
  value: "deepseek-ai/DeepSeek-V3.1-Terminus",
87
  label: "DeepSeek V3.1 Terminus",
88
  providers: ["novita"],
 
89
  isNew: true,
90
  }
91
  ];
 
86
  value: "deepseek-ai/DeepSeek-V3.1-Terminus",
87
  label: "DeepSeek V3.1 Terminus",
88
  providers: ["novita"],
89
+ autoProvider: "novita",
90
  isNew: true,
91
  }
92
  ];