enzostvs HF Staff commited on
Commit
9e87e57
·
1 Parent(s): 318e485

🖼️ upload emoji

Browse files
actions/projects.ts CHANGED
@@ -14,9 +14,21 @@ import { Commit, File } from "@/lib/type";
14
 
15
  export interface ProjectWithCommits extends SpaceEntry {
16
  commits?: Commit[];
 
17
  }
18
 
19
  const IGNORED_PATHS = ["README.md", ".gitignore", ".gitattributes"];
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  export const getProjects = async () => {
22
  const projects: SpaceEntry[] = [];
@@ -61,18 +73,53 @@ export const getProject = async (id: string, commitId?: string) => {
61
  name: id,
62
  };
63
  const files: File[] = [];
 
64
  const params = { repo, accessToken: token };
65
  if (commitId) {
66
  Object.assign(params, { revision: commitId });
67
  }
68
  for await (const fileInfo of listFiles(params)) {
69
  if (IGNORED_PATHS.includes(fileInfo.path)) continue;
70
- if (
71
- fileInfo.path.endsWith(".html") ||
72
- fileInfo.path.endsWith(".css") ||
73
- fileInfo.path.endsWith(".js") ||
74
- fileInfo.path.endsWith(".json")
75
- ) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  const blob = await downloadFile({
77
  repo,
78
  accessToken: token,
@@ -94,46 +141,17 @@ export const getProject = async (id: string, commitId?: string) => {
94
  content: html,
95
  });
96
  }
97
- if (fileInfo.type === "directory") {
98
- for await (const subFile of listFiles({
99
- repo,
100
- accessToken: token,
101
- path: fileInfo.path,
102
- })) {
103
- if (
104
- subFile.path.endsWith(".html") ||
105
- subFile.path.endsWith(".css") ||
106
- subFile.path.endsWith(".js") ||
107
- subFile.path.endsWith(".json")
108
- ) {
109
- const blob = await downloadFile({
110
- repo,
111
- accessToken: token,
112
- path: subFile.path,
113
- raw: true,
114
- ...(commitId ? { revision: commitId } : {}),
115
- }).catch((_) => {
116
- return null;
117
- });
118
- if (!blob) {
119
- continue;
120
- }
121
- const html = await blob?.text();
122
- if (!html) {
123
- continue;
124
- }
125
- files[subFile.path === "index.html" ? "unshift" : "push"]({
126
- path: subFile.path,
127
- content: html,
128
- });
129
- }
130
- }
131
- }
132
  }
133
  const commits: Commit[] = [];
134
  const commitIterator = listCommits({ repo, accessToken: token });
135
  for await (const commit of commitIterator) {
136
- if (commit.title?.toLowerCase() === "initial commit") continue;
 
 
 
 
 
 
137
  commits.push({
138
  title: commit.title,
139
  oid: commit.oid,
@@ -145,6 +163,7 @@ export const getProject = async (id: string, commitId?: string) => {
145
  }
146
 
147
  project.commits = commits;
 
148
 
149
  return { project, files };
150
  } catch (error) {
 
14
 
15
  export interface ProjectWithCommits extends SpaceEntry {
16
  commits?: Commit[];
17
+ medias?: string[];
18
  }
19
 
20
  const IGNORED_PATHS = ["README.md", ".gitignore", ".gitattributes"];
21
+ const IGNORED_FORMATS = [
22
+ ".png",
23
+ ".jpg",
24
+ ".jpeg",
25
+ ".gif",
26
+ ".svg",
27
+ ".webp",
28
+ ".mp4",
29
+ ".mp3",
30
+ ".wav",
31
+ ];
32
 
33
  export const getProjects = async () => {
34
  const projects: SpaceEntry[] = [];
 
73
  name: id,
74
  };
75
  const files: File[] = [];
76
+ const medias: string[] = [];
77
  const params = { repo, accessToken: token };
78
  if (commitId) {
79
  Object.assign(params, { revision: commitId });
80
  }
81
  for await (const fileInfo of listFiles(params)) {
82
  if (IGNORED_PATHS.includes(fileInfo.path)) continue;
83
+ if (IGNORED_FORMATS.some((format) => fileInfo.path.endsWith(format))) {
84
+ medias.push(
85
+ `https://huggingface.co/spaces/${id}/resolve/main/${fileInfo.path}`
86
+ );
87
+ continue;
88
+ }
89
+
90
+ if (fileInfo.type === "directory") {
91
+ for await (const subFile of listFiles({
92
+ repo,
93
+ accessToken: token,
94
+ path: fileInfo.path,
95
+ })) {
96
+ if (IGNORED_FORMATS.some((format) => subFile.path.endsWith(format))) {
97
+ medias.push(
98
+ `https://huggingface.co/spaces/${id}/resolve/main/${subFile.path}`
99
+ );
100
+ }
101
+ const blob = await downloadFile({
102
+ repo,
103
+ accessToken: token,
104
+ path: subFile.path,
105
+ raw: true,
106
+ ...(commitId ? { revision: commitId } : {}),
107
+ }).catch((_) => {
108
+ return null;
109
+ });
110
+ if (!blob) {
111
+ continue;
112
+ }
113
+ const html = await blob?.text();
114
+ if (!html) {
115
+ continue;
116
+ }
117
+ files[subFile.path === "index.html" ? "unshift" : "push"]({
118
+ path: subFile.path,
119
+ content: html,
120
+ });
121
+ }
122
+ } else {
123
  const blob = await downloadFile({
124
  repo,
125
  accessToken: token,
 
141
  content: html,
142
  });
143
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  }
145
  const commits: Commit[] = [];
146
  const commitIterator = listCommits({ repo, accessToken: token });
147
  for await (const commit of commitIterator) {
148
+ if (
149
+ commit.title?.toLowerCase() === "initial commit" ||
150
+ commit.title
151
+ ?.toLowerCase()
152
+ ?.includes("upload media files through deepsite")
153
+ )
154
+ continue;
155
  commits.push({
156
  title: commit.title,
157
  oid: commit.oid,
 
163
  }
164
 
165
  project.commits = commits;
166
+ project.medias = medias;
167
 
168
  return { project, files };
169
  } catch (error) {
app/api/ask/route.ts CHANGED
@@ -20,9 +20,9 @@ export async function POST(request: Request) {
20
  previousMessages = [],
21
  files = [],
22
  provider: initialProvider,
23
- mentions = [],
24
  model,
25
  redesignMd,
 
26
  } = body;
27
  const provider = initialProvider ?? "auto";
28
 
@@ -81,7 +81,13 @@ export async function POST(request: Request) {
81
  content: `${
82
  redesignMd?.url &&
83
  `Redesign the following website ${redesignMd.url}, try to use the same images and content, but you can still improve it if needed. Do the best version possibile. Here is the markdown:\n ${redesignMd.md} \n\n`
84
- }${prompt}`,
 
 
 
 
 
 
85
  },
86
  ],
87
  stream: true,
 
20
  previousMessages = [],
21
  files = [],
22
  provider: initialProvider,
 
23
  model,
24
  redesignMd,
25
+ medias,
26
  } = body;
27
  const provider = initialProvider ?? "auto";
28
 
 
81
  content: `${
82
  redesignMd?.url &&
83
  `Redesign the following website ${redesignMd.url}, try to use the same images and content, but you can still improve it if needed. Do the best version possibile. Here is the markdown:\n ${redesignMd.md} \n\n`
84
+ }${prompt} ${
85
+ medias && medias.length > 0
86
+ ? `\nHere is the list of my media files: ${medias.join(
87
+ ", "
88
+ )}\n`
89
+ : ""
90
+ }`,
91
  },
92
  ],
93
  stream: true,
app/api/projects/[repoId]/medias/route.ts ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { auth } from "@/lib/auth";
2
+ import { createBranch, RepoDesignation, uploadFiles } from "@huggingface/hub";
3
+ import { format } from "date-fns";
4
+ import { NextResponse } from "next/server";
5
+
6
+ export async function POST(
7
+ request: Request,
8
+ { params }: { params: Promise<{ repoId: string }> }
9
+ ) {
10
+ const { repoId }: { repoId: string } = await params;
11
+ const session = await auth();
12
+ if (!session) {
13
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
14
+ }
15
+ const token = session.accessToken;
16
+
17
+ const repo: RepoDesignation = {
18
+ type: "space",
19
+ name: session.user?.username + "/" + repoId,
20
+ };
21
+
22
+ const formData = await request.formData();
23
+ const newMedias = formData.getAll("images") as File[];
24
+
25
+ const filesToUpload: File[] = [];
26
+
27
+ if (!newMedias || newMedias.length === 0) {
28
+ return NextResponse.json(
29
+ {
30
+ ok: false,
31
+ error: "At least one media file is required under the 'images' key",
32
+ },
33
+ { status: 400 }
34
+ );
35
+ }
36
+
37
+ try {
38
+ for (const media of newMedias) {
39
+ const isImage = media.type.startsWith("image/");
40
+ const isVideo = media.type.startsWith("video/");
41
+ const isAudio = media.type.startsWith("audio/");
42
+
43
+ const folderPath = isImage
44
+ ? "images/"
45
+ : isVideo
46
+ ? "videos/"
47
+ : isAudio
48
+ ? "audios/"
49
+ : null;
50
+
51
+ if (!folderPath) {
52
+ return NextResponse.json(
53
+ { ok: false, error: "Unsupported media type: " + media.type },
54
+ { status: 400 }
55
+ );
56
+ }
57
+
58
+ const mediaName = `${folderPath}${media.name}`;
59
+ const processedFile = new File([media], mediaName, { type: media.type });
60
+ filesToUpload.push(processedFile);
61
+ }
62
+
63
+ await uploadFiles({
64
+ repo,
65
+ files: filesToUpload,
66
+ accessToken: token,
67
+ commitTitle: `📁 Upload media files through DeepSite`,
68
+ });
69
+
70
+ return NextResponse.json(
71
+ {
72
+ success: true,
73
+ medias: filesToUpload.map(
74
+ (file) =>
75
+ `https://huggingface.co/spaces/${session.user?.username}/${repoId}/resolve/main/${file.name}`
76
+ ),
77
+ },
78
+ { status: 200 }
79
+ );
80
+ } catch (error) {
81
+ console.log(error);
82
+ return NextResponse.json(
83
+ { ok: false, error: error ?? "Failed to upload media files" },
84
+ { status: 500 }
85
+ );
86
+ }
87
+
88
+ return NextResponse.json({ success: true }, { status: 200 });
89
+ }
components/ask-ai/ask-ai.tsx CHANGED
@@ -20,6 +20,7 @@ export function AskAI({
20
  className,
21
  onToggleMobileTab,
22
  files,
 
23
  isNew = false,
24
  isHistoryView,
25
  projectName = "new",
@@ -27,6 +28,7 @@ export function AskAI({
27
  initialPrompt?: string;
28
  className?: string;
29
  files?: File[] | null;
 
30
  onToggleMobileTab?: (tab: MobileTabType) => void;
31
  isNew?: boolean;
32
  isHistoryView?: boolean;
@@ -46,6 +48,7 @@ export function AskAI({
46
  md: string;
47
  url: string;
48
  } | null>(null);
 
49
 
50
  const router = useRouter();
51
  const { callAi, isLoading, stopGeneration, audio } =
@@ -83,7 +86,14 @@ export function AskAI({
83
  if (contentEditableRef.current) {
84
  contentEditableRef.current.innerHTML = "";
85
  }
86
- callAi({ prompt, model, onComplete, provider, redesignMd });
 
 
 
 
 
 
 
87
  };
88
 
89
  return (
@@ -103,7 +113,11 @@ export function AskAI({
103
  />
104
  <footer className="flex items-center justify-between mt-0">
105
  <div className="flex items-center gap-1.5">
106
- <Uploader />
 
 
 
 
107
  <Models
108
  model={model}
109
  setModel={setModel}
 
20
  className,
21
  onToggleMobileTab,
22
  files,
23
+ medias,
24
  isNew = false,
25
  isHistoryView,
26
  projectName = "new",
 
28
  initialPrompt?: string;
29
  className?: string;
30
  files?: File[] | null;
31
+ medias?: string[] | null;
32
  onToggleMobileTab?: (tab: MobileTabType) => void;
33
  isNew?: boolean;
34
  isHistoryView?: boolean;
 
48
  md: string;
49
  url: string;
50
  } | null>(null);
51
+ const [selectedMedias, setSelectedMedias] = useState<string[]>([]);
52
 
53
  const router = useRouter();
54
  const { callAi, isLoading, stopGeneration, audio } =
 
86
  if (contentEditableRef.current) {
87
  contentEditableRef.current.innerHTML = "";
88
  }
89
+ callAi({
90
+ prompt,
91
+ model,
92
+ onComplete,
93
+ provider,
94
+ redesignMd,
95
+ medias: selectedMedias ?? [],
96
+ });
97
  };
98
 
99
  return (
 
113
  />
114
  <footer className="flex items-center justify-between mt-0">
115
  <div className="flex items-center gap-1.5">
116
+ <Uploader
117
+ medias={medias}
118
+ selected={selectedMedias}
119
+ setSelected={setSelectedMedias}
120
+ />
121
  <Models
122
  model={model}
123
  setModel={setModel}
components/ask-ai/uploader.tsx CHANGED
@@ -1,5 +1,14 @@
1
- import { Paperclip } from "lucide-react";
2
- import { useState } from "react";
 
 
 
 
 
 
 
 
 
3
 
4
  import {
5
  Popover,
@@ -7,30 +16,216 @@ import {
7
  PopoverTrigger,
8
  } from "@/components/ui/popover";
9
  import { Button } from "@/components/ui/button";
 
 
 
 
 
10
  // todo: implement the uploader component
11
 
12
- export const Uploader = () => {
 
 
 
 
 
 
 
 
 
 
 
13
  const [open, setOpen] = useState(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  return (
15
  <div className="flex items-center justify-start gap-1 flex-wrap">
16
  <Popover open={open} onOpenChange={setOpen}>
17
  <PopoverTrigger asChild>
18
  <Button
19
- size="icon-xs"
20
- variant={open ? "default" : "bordered"}
21
- className="rounded-full!"
 
 
22
  >
23
  <Paperclip className="size-3" />
 
 
 
 
 
24
  </Button>
25
  </PopoverTrigger>
26
- <PopoverContent
27
- align="start"
28
- className="space-y-4 min-w-fit rounded-2xl! p-0!"
29
- >
30
- <main className="p-4">
31
- <p className="text-xs text-muted-foreground mb-2.5">
32
- Upload a file
 
 
 
 
 
 
 
 
 
33
  </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  </main>
35
  </PopoverContent>
36
  </Popover>
 
1
+ import {
2
+ CheckCircle,
3
+ FileVideo,
4
+ ImageIcon,
5
+ Music,
6
+ Paperclip,
7
+ Video,
8
+ } from "lucide-react";
9
+ import { useRef, useState } from "react";
10
+ import Image from "next/image";
11
+ import { useParams } from "next/navigation";
12
 
13
  import {
14
  Popover,
 
16
  PopoverTrigger,
17
  } from "@/components/ui/popover";
18
  import { Button } from "@/components/ui/button";
19
+ import { getFileType, humanizeNumber } from "@/lib/utils";
20
+ import { useQueryClient } from "@tanstack/react-query";
21
+ import { ProjectWithCommits } from "@/actions/projects";
22
+ import { toast } from "sonner";
23
+ import Loading from "../loading";
24
  // todo: implement the uploader component
25
 
26
+ export const Uploader = ({
27
+ medias,
28
+ selected,
29
+ setSelected,
30
+ }: {
31
+ medias?: string[] | null;
32
+ selected: string[];
33
+ setSelected: React.Dispatch<React.SetStateAction<string[]>>;
34
+ }) => {
35
+ const queryClient = useQueryClient();
36
+ const { repoId } = useParams<{ repoId: string }>();
37
+
38
  const [open, setOpen] = useState(false);
39
+ const [isUploading, setIsUploading] = useState(false);
40
+ const [error, setError] = useState<string | null>(null);
41
+ const fileInputRef = useRef<HTMLInputElement>(null);
42
+
43
+ const getFileIcon = (url: string) => {
44
+ const fileType = getFileType(url);
45
+ switch (fileType) {
46
+ case "image":
47
+ return <ImageIcon className="size-4" />;
48
+ case "video":
49
+ return <Video className="size-4" />;
50
+ case "audio":
51
+ return <Music className="size-4" />;
52
+ default:
53
+ return <FileVideo className="size-4" />;
54
+ }
55
+ };
56
+
57
+ const uploadFiles = async (files: FileList | null) => {
58
+ setError(null);
59
+ if (!files || files.length === 0) return;
60
+
61
+ setIsUploading(true);
62
+ const data = new FormData();
63
+ Array.from(files).forEach((file) => {
64
+ data.append("images", file);
65
+ });
66
+
67
+ const response = await fetch(`/api/projects/${repoId}/medias`, {
68
+ method: "POST",
69
+ body: data,
70
+ })
71
+ .then(async (response) => {
72
+ if (response.ok) {
73
+ const data = await response.json();
74
+ return data;
75
+ }
76
+ throw new Error("Failed to save changes");
77
+ })
78
+ .catch((err) => {
79
+ return { success: false, err };
80
+ });
81
+
82
+ if (response.success) {
83
+ queryClient.setQueryData(
84
+ ["project"],
85
+ (oldProject: ProjectWithCommits) => ({
86
+ ...oldProject,
87
+ medias: [...response.medias, ...(oldProject?.medias ?? [])],
88
+ })
89
+ );
90
+ toast.success("Media files uploaded successfully!");
91
+ } else {
92
+ setError(
93
+ response.err ?? "Failed to upload media files, try again later."
94
+ );
95
+ }
96
+
97
+ setIsUploading(false);
98
+ };
99
+
100
  return (
101
  <div className="flex items-center justify-start gap-1 flex-wrap">
102
  <Popover open={open} onOpenChange={setOpen}>
103
  <PopoverTrigger asChild>
104
  <Button
105
+ size={selected?.length > 0 ? "xs" : "icon-xs"}
106
+ variant={
107
+ selected?.length > 0 ? "indigo" : open ? "default" : "bordered"
108
+ }
109
+ className="rounded-full! relative"
110
  >
111
  <Paperclip className="size-3" />
112
+ {selected?.length > 0 && (
113
+ <span className="py-0.5 px-1 text-[10px] flex items-center justify-center rounded-full font-semibold bg-white/20 dark:bg-indigo-500 dark:text-white font-mono min-w-4">
114
+ {humanizeNumber(selected.length)}
115
+ </span>
116
+ )}
117
  </Button>
118
  </PopoverTrigger>
119
+ <PopoverContent className="translate-x-6 rounded-2xl! p-0! bg-accent! border-border-muted! w-86 text-center overflow-hidden">
120
+ <header className="bg-linear-to-b from-indigo-500/15 dark:from-indigo-500/40 to-accent p-6">
121
+ <div className="flex items-center justify-center -space-x-4 mb-3">
122
+ <div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
123
+ 📷
124
+ </div>
125
+ <div className="size-11 rounded-full bg-yellow-200 shadow-2xl flex items-center justify-center text-2xl z-2">
126
+ 🖼️
127
+ </div>
128
+ <div className="size-9 rounded-full bg-green-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
129
+ 📁
130
+ </div>
131
+ </div>
132
+ <p className="text-xl font-semibold text-primary">Upload Files</p>
133
+ <p className="text-sm text-muted-foreground mt-1.5">
134
+ Upload files to include them in the AI context.
135
  </p>
136
+ </header>
137
+ <main className="space-y-4 px-6 pb-6">
138
+ {error && <p className="text-sm text-red-500">{error}</p>}
139
+ {medias && medias.length > 0 && (
140
+ <div>
141
+ <p className="text-sm text-muted-foreground mb-2">
142
+ Uploaded files:
143
+ </p>
144
+ <div className="flex flex-col gap-2 max-h-60 overflow-y-auto">
145
+ <div className="grid grid-cols-4 gap-1.5 flex-wrap max-h-40 overflow-y-auto">
146
+ {medias.map((media: string) => {
147
+ const fileType = getFileType(media);
148
+ return (
149
+ <div
150
+ key={media}
151
+ className="select-none relative cursor-pointer bg-accent rounded-md border-2 border-border transition-all duration-300"
152
+ onClick={() =>
153
+ setSelected(
154
+ selected.includes(media)
155
+ ? selected.filter((f) => f !== media)
156
+ : [...selected, media]
157
+ )
158
+ }
159
+ >
160
+ {fileType === "image" ? (
161
+ <Image
162
+ src={media}
163
+ alt="uploaded image"
164
+ width={56}
165
+ height={56}
166
+ className="object-contain w-full rounded-sm aspect-square h-14"
167
+ />
168
+ ) : fileType === "video" ? (
169
+ <div className="w-full h-14 rounded-sm bg-gray-100 flex items-center justify-center relative">
170
+ <video
171
+ src={media}
172
+ className="w-full h-full object-cover rounded-sm"
173
+ muted
174
+ preload="metadata"
175
+ />
176
+ <div className="absolute inset-0 flex items-center justify-center bg-black/20 rounded-sm">
177
+ <Video className="size-4 text-white" />
178
+ </div>
179
+ </div>
180
+ ) : fileType === "audio" ? (
181
+ <div className="w-full h-14 rounded-sm bg-linear-to-br from-purple-100 to-pink-100 flex items-center justify-center">
182
+ <Music className="size-6 text-purple-600" />
183
+ </div>
184
+ ) : (
185
+ <div className="w-full h-14 rounded-sm bg-gray-100 flex items-center justify-center">
186
+ {getFileIcon(media)}
187
+ </div>
188
+ )}
189
+ {selected.includes(media) && (
190
+ <div className="absolute top-0 right-0 h-full w-full flex items-center justify-center bg-black/50 rounded-md">
191
+ <CheckCircle className="size-6 text-neutral-100" />
192
+ </div>
193
+ )}
194
+ </div>
195
+ );
196
+ })}
197
+ </div>
198
+ </div>
199
+ </div>
200
+ )}
201
+ <div>
202
+ <Button
203
+ variant="default"
204
+ onClick={() => fileInputRef.current?.click()}
205
+ className="relative w-full"
206
+ disabled={isUploading}
207
+ >
208
+ {isUploading ? (
209
+ <>
210
+ <Loading
211
+ overlay={false}
212
+ className="ml-2 size-4 animate-spin text-primary-foreground"
213
+ />
214
+ Uploading media file(s)...
215
+ </>
216
+ ) : (
217
+ "Upload Media Files"
218
+ )}
219
+ </Button>
220
+ <input
221
+ ref={fileInputRef}
222
+ type="file"
223
+ className="hidden"
224
+ multiple
225
+ accept="image/*,video/*,audio/*,.mp3,.mp4,.wav,.aac,.m4a,.ogg,.webm,.avi,.mov"
226
+ onChange={(e) => uploadFiles(e.target.files)}
227
+ />
228
+ </div>
229
  </main>
230
  </PopoverContent>
231
  </Popover>
components/ask-ai/useGeneration.ts CHANGED
@@ -193,6 +193,7 @@ export const useGeneration = (projectName: string) => {
193
  onComplete,
194
  provider = "auto",
195
  redesignMd,
 
196
  }: {
197
  prompt: string;
198
  model: string;
@@ -200,6 +201,7 @@ export const useGeneration = (projectName: string) => {
200
  url: string;
201
  md: string;
202
  } | null;
 
203
  onComplete: () => void;
204
  provider?: ProviderType;
205
  }) => {
@@ -234,6 +236,7 @@ export const useGeneration = (projectName: string) => {
234
  previousMessages,
235
  provider,
236
  redesignMd,
 
237
  }),
238
  headers: {
239
  "Content-Type": "application/json",
 
193
  onComplete,
194
  provider = "auto",
195
  redesignMd,
196
+ medias,
197
  }: {
198
  prompt: string;
199
  model: string;
 
201
  url: string;
202
  md: string;
203
  } | null;
204
+ medias?: string[] | null;
205
  onComplete: () => void;
206
  provider?: ProviderType;
207
  }) => {
 
236
  previousMessages,
237
  provider,
238
  redesignMd,
239
+ medias,
240
  }),
241
  headers: {
242
  "Content-Type": "application/json",
components/code/index.tsx CHANGED
@@ -15,7 +15,7 @@ import { cn } from "@/lib/utils";
15
  import Loading from "../loading";
16
  import { ProjectWithCommits } from "@/actions/projects";
17
 
18
- // todo: while no them setted but in dark mode, the dark theme is not setted correctly.
19
 
20
  export function AppEditorCode() {
21
  const queryClient = useQueryClient();
 
15
  import Loading from "../loading";
16
  import { ProjectWithCommits } from "@/actions/projects";
17
 
18
+ // todo: while no theme setted but in dark mode, the dark theme is not setted correctly.
19
 
20
  export function AppEditorCode() {
21
  const queryClient = useQueryClient();
components/editor/header.tsx CHANGED
@@ -96,11 +96,15 @@ export function AppEditorHeader({
96
  <>
97
  {!isHistoryView && (
98
  <Button
99
- variant="indigo"
100
  size="xs"
101
  onClick={handleToggleActivity}
102
  >
103
- {currentActivity === "chat" ? <Code /> : <MessageCircle />}
 
 
 
 
104
  {currentActivity === "chat" ? "Code" : "Chat"}
105
  </Button>
106
  )}
@@ -112,12 +116,12 @@ export function AppEditorHeader({
112
  >
113
  {device === "desktop" ? (
114
  <>
115
- <Monitor className="size-4" />
116
  Desktop
117
  </>
118
  ) : (
119
  <>
120
- <Smartphone className="size-4" />
121
  Mobile
122
  </>
123
  )}
@@ -138,7 +142,7 @@ export function AppEditorHeader({
138
  variant="bordered"
139
  className="h-7 px-2! overflow-hidden gap-0"
140
  >
141
- <ExternalLink className="size-4 shrink-0" />
142
  <motion.span
143
  initial={false}
144
  animate={{
 
96
  <>
97
  {!isHistoryView && (
98
  <Button
99
+ variant="bordered"
100
  size="xs"
101
  onClick={handleToggleActivity}
102
  >
103
+ {currentActivity === "chat" ? (
104
+ <Code className="size-3.5" />
105
+ ) : (
106
+ <MessageCircle className="size-3.5" />
107
+ )}
108
  {currentActivity === "chat" ? "Code" : "Chat"}
109
  </Button>
110
  )}
 
116
  >
117
  {device === "desktop" ? (
118
  <>
119
+ <Monitor className="size-3.5" />
120
  Desktop
121
  </>
122
  ) : (
123
  <>
124
+ <Smartphone className="size-3.5" />
125
  Mobile
126
  </>
127
  )}
 
142
  variant="bordered"
143
  className="h-7 px-2! overflow-hidden gap-0"
144
  >
145
+ <ExternalLink className="size-3.5 shrink-0" />
146
  <motion.span
147
  initial={false}
148
  animate={{
components/editor/index.tsx CHANGED
@@ -128,6 +128,7 @@ export function AppEditor({
128
  projectName={projectName}
129
  initialPrompt={initialPrompt}
130
  files={files}
 
131
  onToggleMobileTab={setMobileTab}
132
  />
133
  </div>
 
128
  projectName={projectName}
129
  initialPrompt={initialPrompt}
130
  files={files}
131
+ medias={project?.medias ?? []}
132
  onToggleMobileTab={setMobileTab}
133
  />
134
  </div>
lib/prompts.ts CHANGED
@@ -6,7 +6,7 @@ export const SEARCH_START = "=== SEARCH";
6
  export const DIVIDER = "=======";
7
  export const REPLACE_END = "=== REPLACE";
8
 
9
- // todo: rework follow up prompt, take a look at what I did on the v3 version, was working better.
10
 
11
  export const PROMPT_FOR_IMAGE_GENERATION = `
12
  === IMAGE PLACEHOLDER ===
 
6
  export const DIVIDER = "=======";
7
  export const REPLACE_END = "=== REPLACE";
8
 
9
+ // following prompt doesnt work well. what it does sometimes it to use the START_FILE_CONTENT and END_FILE_CONTENT markers with comments which says "keep original part here", which is not correct, it should use SEARCH/REPLACE format instead.
10
 
11
  export const PROMPT_FOR_IMAGE_GENERATION = `
12
  === IMAGE PLACEHOLDER ===
lib/utils.ts CHANGED
@@ -57,7 +57,8 @@ export const getContextFilesFromPrompt = async (
57
  ) => {
58
  const mentions = prompt.match(/file:\/[a-zA-Z0-9-_.\/]+/g);
59
  const filesToUseAsContext: File[] = [];
60
- if (currentFiles.length === 0) return filesToUseAsContext;
 
61
  mentions?.forEach((mention) => {
62
  const filePath = mention.replace("file:/", "");
63
  const matchedFile = currentFiles.find((file) => file.path === filePath);
@@ -141,3 +142,18 @@ export const humanizeNumber = (num: number): string => {
141
  }
142
  return num.toString();
143
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  ) => {
58
  const mentions = prompt.match(/file:\/[a-zA-Z0-9-_.\/]+/g);
59
  const filesToUseAsContext: File[] = [];
60
+ if (currentFiles.length === 0) return currentFiles;
61
+ if (!mentions || mentions.length === 0) return currentFiles;
62
  mentions?.forEach((mention) => {
63
  const filePath = mention.replace("file:/", "");
64
  const matchedFile = currentFiles.find((file) => file.path === filePath);
 
142
  }
143
  return num.toString();
144
  };
145
+
146
+ export const getFileType = (url: string) => {
147
+ if (typeof url !== "string") {
148
+ return "unknown";
149
+ }
150
+ const extension = url.split(".").pop()?.toLowerCase();
151
+ if (["jpg", "jpeg", "png", "gif", "webp", "svg"].includes(extension || "")) {
152
+ return "image";
153
+ } else if (["mp4", "webm", "ogg", "avi", "mov"].includes(extension || "")) {
154
+ return "video";
155
+ } else if (["mp3", "wav", "ogg", "aac", "m4a"].includes(extension || "")) {
156
+ return "audio";
157
+ }
158
+ return "unknown";
159
+ };