Skip to content

Commit 8c54ca2

Browse files
committed
refactor(ux): combined sources to one section
- Renamed "Documents" section to "Sources" in the dashboard layout. - Updated routing for adding sources and managing documents. - Refactored the connectors and documents upload pages to redirect to the new sources section. - Added localization support for the new "Sources" terminology in English and Chinese.
1 parent ed348b1 commit 8c54ca2

File tree

16 files changed

+1188
-1332
lines changed

16 files changed

+1188
-1332
lines changed

surfsense_web/app/dashboard/[search_space_id]/connectors/add/page.tsx

Lines changed: 9 additions & 407 deletions
Large diffs are not rendered by default.

surfsense_web/app/dashboard/[search_space_id]/documents/upload/page.tsx

Lines changed: 7 additions & 571 deletions
Large diffs are not rendered by default.
Lines changed: 6 additions & 292 deletions
Original file line numberDiff line numberDiff line change
@@ -1,302 +1,16 @@
11
"use client";
22

3-
import { IconBrandYoutube } from "@tabler/icons-react";
4-
import { type Tag, TagInput } from "emblor";
5-
import { Loader2 } from "lucide-react";
6-
import { motion, type Variants } from "motion/react";
73
import { useParams, useRouter } from "next/navigation";
8-
import { useTranslations } from "next-intl";
9-
import { useState } from "react";
10-
import { toast } from "sonner";
11-
import { Button } from "@/components/ui/button";
12-
import {
13-
Card,
14-
CardContent,
15-
CardDescription,
16-
CardFooter,
17-
CardHeader,
18-
CardTitle,
19-
} from "@/components/ui/card";
20-
import { Label } from "@/components/ui/label";
4+
import { useEffect } from "react";
215

22-
// YouTube video ID validation regex
23-
const youtubeRegex =
24-
/^(https:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})$/;
25-
26-
export default function YouTubeVideoAdder() {
27-
const t = useTranslations("add_youtube");
6+
export default function YouTubeRedirect() {
287
const params = useParams();
298
const router = useRouter();
309
const search_space_id = params.search_space_id as string;
3110

32-
const [videoTags, setVideoTags] = useState<Tag[]>([]);
33-
const [activeTagIndex, setActiveTagIndex] = useState<number | null>(null);
34-
const [isSubmitting, setIsSubmitting] = useState(false);
35-
const [error, setError] = useState<string | null>(null);
36-
37-
// Function to validate a YouTube URL
38-
const isValidYoutubeUrl = (url: string): boolean => {
39-
return youtubeRegex.test(url);
40-
};
41-
42-
// Function to extract video ID from URL
43-
const extractVideoId = (url: string): string | null => {
44-
const match = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/);
45-
return match ? match[1] : null;
46-
};
47-
48-
// Function to handle video URL submission
49-
const handleSubmit = async () => {
50-
// Validate that we have at least one video URL
51-
if (videoTags.length === 0) {
52-
setError(t("error_no_video"));
53-
return;
54-
}
55-
56-
// Validate all URLs
57-
const invalidUrls = videoTags.filter((tag) => !isValidYoutubeUrl(tag.text));
58-
if (invalidUrls.length > 0) {
59-
setError(t("error_invalid_urls", { urls: invalidUrls.map((tag) => tag.text).join(", ") }));
60-
return;
61-
}
62-
63-
setError(null);
64-
setIsSubmitting(true);
65-
66-
try {
67-
toast(t("processing_toast"), {
68-
description: t("processing_toast_desc"),
69-
});
70-
71-
// Extract URLs from tags
72-
const videoUrls = videoTags.map((tag) => tag.text);
73-
74-
// Make API call to backend
75-
const response = await fetch(
76-
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents`,
77-
{
78-
method: "POST",
79-
headers: {
80-
"Content-Type": "application/json",
81-
Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`,
82-
},
83-
body: JSON.stringify({
84-
document_type: "YOUTUBE_VIDEO",
85-
content: videoUrls,
86-
search_space_id: parseInt(search_space_id),
87-
}),
88-
}
89-
);
90-
91-
if (!response.ok) {
92-
throw new Error("Failed to process YouTube videos");
93-
}
94-
95-
await response.json();
96-
97-
toast(t("success_toast"), {
98-
description: t("success_toast_desc"),
99-
});
100-
101-
// Redirect to documents page
102-
router.push(`/dashboard/${search_space_id}/documents`);
103-
} catch (error: any) {
104-
setError(error.message || t("error_generic"));
105-
toast(t("error_toast"), {
106-
description: `${t("error_toast_desc")}: ${error.message}`,
107-
});
108-
} finally {
109-
setIsSubmitting(false);
110-
}
111-
};
112-
113-
// Function to add a new video URL tag
114-
const handleAddTag = (text: string) => {
115-
// Basic URL validation
116-
if (!isValidYoutubeUrl(text)) {
117-
toast(t("invalid_url_toast"), {
118-
description: t("invalid_url_toast_desc"),
119-
});
120-
return;
121-
}
122-
123-
// Check for duplicates
124-
if (videoTags.some((tag) => tag.text === text)) {
125-
toast(t("duplicate_url_toast"), {
126-
description: t("duplicate_url_toast_desc"),
127-
});
128-
return;
129-
}
130-
131-
// Add the new tag
132-
const newTag: Tag = {
133-
id: Date.now().toString(),
134-
text: text,
135-
};
136-
137-
setVideoTags([...videoTags, newTag]);
138-
};
139-
140-
// Animation variants
141-
const containerVariants: Variants = {
142-
hidden: { opacity: 0 },
143-
visible: {
144-
opacity: 1,
145-
transition: {
146-
staggerChildren: 0.1,
147-
},
148-
},
149-
};
150-
151-
const itemVariants: Variants = {
152-
hidden: { y: 20, opacity: 0 },
153-
visible: {
154-
y: 0,
155-
opacity: 1,
156-
transition: {
157-
type: "spring",
158-
stiffness: 300,
159-
damping: 24,
160-
},
161-
},
162-
};
163-
164-
return (
165-
<div className="container mx-auto py-8">
166-
<motion.div initial="hidden" animate="visible" variants={containerVariants}>
167-
<Card className="max-w-2xl mx-auto">
168-
<motion.div variants={itemVariants}>
169-
<CardHeader>
170-
<CardTitle className="flex items-center gap-2">
171-
<IconBrandYoutube className="h-5 w-5" />
172-
{t("title")}
173-
</CardTitle>
174-
<CardDescription>{t("subtitle")}</CardDescription>
175-
</CardHeader>
176-
</motion.div>
177-
178-
<motion.div variants={itemVariants}>
179-
<CardContent>
180-
<div className="space-y-4">
181-
<div className="space-y-2">
182-
<Label htmlFor="video-input">{t("label")}</Label>
183-
<TagInput
184-
id="video-input"
185-
tags={videoTags}
186-
setTags={setVideoTags}
187-
placeholder={t("placeholder")}
188-
onAddTag={handleAddTag}
189-
styleClasses={{
190-
inlineTagsContainer:
191-
"border-input rounded-lg bg-background shadow-sm shadow-black/5 transition-shadow focus-within:border-ring focus-within:outline-none focus-within:ring-[3px] focus-within:ring-ring/20 p-1 gap-1",
192-
input: "w-full min-w-[80px] focus-visible:outline-none shadow-none px-2 h-7",
193-
tag: {
194-
body: "h-7 relative bg-background border border-input hover:bg-background rounded-md font-medium text-xs ps-2 pe-7 flex",
195-
closeButton:
196-
"absolute -inset-y-px -end-px p-0 rounded-e-lg flex size-7 transition-colors outline-0 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring/70 text-muted-foreground/80 hover:text-foreground",
197-
},
198-
}}
199-
activeTagIndex={activeTagIndex}
200-
setActiveTagIndex={setActiveTagIndex}
201-
/>
202-
<p className="text-xs text-muted-foreground mt-1">{t("hint")}</p>
203-
</div>
204-
205-
{error && (
206-
<motion.div
207-
className="text-sm text-red-500 mt-2"
208-
initial={{ opacity: 0, scale: 0.9 }}
209-
animate={{ opacity: 1, scale: 1 }}
210-
transition={{ type: "spring", stiffness: 500, damping: 30 }}
211-
>
212-
{error}
213-
</motion.div>
214-
)}
215-
216-
<motion.div variants={itemVariants} className="bg-muted/50 rounded-lg p-4 text-sm">
217-
<h4 className="font-medium mb-2">{t("tips_title")}</h4>
218-
<ul className="list-disc pl-5 space-y-1 text-muted-foreground">
219-
<li>{t("tip_1")}</li>
220-
<li>{t("tip_2")}</li>
221-
<li>{t("tip_3")}</li>
222-
<li>{t("tip_4")}</li>
223-
</ul>
224-
</motion.div>
225-
226-
{videoTags.length > 0 && (
227-
<motion.div variants={itemVariants} className="mt-4 space-y-2">
228-
<h4 className="font-medium">{t("preview")}:</h4>
229-
<div className="grid grid-cols-1 gap-3">
230-
{videoTags.map((tag, index) => {
231-
const videoId = extractVideoId(tag.text);
232-
return videoId ? (
233-
<motion.div
234-
key={tag.id}
235-
initial={{ opacity: 0, y: 10 }}
236-
animate={{ opacity: 1, y: 0 }}
237-
transition={{ delay: index * 0.1 }}
238-
className="relative aspect-video rounded-lg overflow-hidden border"
239-
>
240-
<iframe
241-
width="100%"
242-
height="100%"
243-
src={`https://www.youtube.com/embed/${videoId}`}
244-
title="YouTube video player"
245-
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
246-
allowFullScreen
247-
></iframe>
248-
</motion.div>
249-
) : null;
250-
})}
251-
</div>
252-
</motion.div>
253-
)}
254-
</div>
255-
</CardContent>
256-
</motion.div>
11+
useEffect(() => {
12+
router.replace(`/dashboard/${search_space_id}/sources/add?tab=youtube`);
13+
}, [search_space_id, router]);
25714

258-
<motion.div variants={itemVariants}>
259-
<CardFooter className="flex justify-between">
260-
<Button
261-
variant="outline"
262-
onClick={() => router.push(`/dashboard/${search_space_id}/documents`)}
263-
>
264-
{t("cancel")}
265-
</Button>
266-
<Button
267-
onClick={handleSubmit}
268-
disabled={isSubmitting || videoTags.length === 0}
269-
className="relative overflow-hidden"
270-
>
271-
{isSubmitting ? (
272-
<>
273-
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
274-
{t("processing")}
275-
</>
276-
) : (
277-
<>
278-
<motion.span
279-
initial={{ x: -5, opacity: 0 }}
280-
animate={{ x: 0, opacity: 1 }}
281-
transition={{ delay: 0.2 }}
282-
className="mr-2"
283-
>
284-
<IconBrandYoutube className="h-4 w-4" />
285-
</motion.span>
286-
{t("submit")}
287-
</>
288-
)}
289-
<motion.div
290-
className="absolute inset-0 bg-primary/10"
291-
initial={{ x: "-100%" }}
292-
animate={isSubmitting ? { x: "0%" } : { x: "-100%" }}
293-
transition={{ duration: 0.5, ease: "easeInOut" }}
294-
/>
295-
</Button>
296-
</CardFooter>
297-
</motion.div>
298-
</Card>
299-
</motion.div>
300-
</div>
301-
);
15+
return null;
30216
}

surfsense_web/app/dashboard/[search_space_id]/layout.tsx

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -41,37 +41,18 @@ export default function DashboardLayout({
4141
},
4242

4343
{
44-
title: "Documents",
44+
title: "Sources",
4545
url: "#",
46-
icon: "FileStack",
46+
icon: "Database",
4747
items: [
4848
{
49-
title: "Upload Documents",
50-
url: `/dashboard/${search_space_id}/documents/upload`,
51-
},
52-
// {
53-
// title: "Add Webpages",
54-
// url: `/dashboard/${search_space_id}/documents/webpage`,
55-
// },
56-
{
57-
title: "Add Youtube Videos",
58-
url: `/dashboard/${search_space_id}/documents/youtube`,
49+
title: "Add Sources",
50+
url: `/dashboard/${search_space_id}/sources/add`,
5951
},
6052
{
6153
title: "Manage Documents",
6254
url: `/dashboard/${search_space_id}/documents`,
6355
},
64-
],
65-
},
66-
{
67-
title: "Connectors",
68-
url: `#`,
69-
icon: "Cable",
70-
items: [
71-
{
72-
title: "Add Connector",
73-
url: `/dashboard/${search_space_id}/connectors/add`,
74-
},
7556
{
7657
title: "Manage Connectors",
7758
url: `/dashboard/${search_space_id}/connectors`,

0 commit comments

Comments
 (0)