mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-26 09:16:22 +02:00
commit
54859fe482
18 changed files with 690 additions and 339 deletions
70
.github/ISSUE_TEMPLATE/README.md
vendored
Normal file
70
.github/ISSUE_TEMPLATE/README.md
vendored
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
# Issue Templates Guide
|
||||||
|
|
||||||
|
Thank you for contributing to SurfSense! To help us address your issue efficiently, please follow these guidelines:
|
||||||
|
|
||||||
|
## 📝 Choosing the Right Template
|
||||||
|
|
||||||
|
- **🐛 Bug Report**: Use this when you encounter unexpected behavior, errors, or crashes
|
||||||
|
- **✨ Feature Request**: Use this to suggest new features or enhancements
|
||||||
|
- **📚 Documentation Issue**: Use this to report problems with documentation
|
||||||
|
|
||||||
|
## 🏷️ Required Labels/Tags
|
||||||
|
|
||||||
|
When creating an issue, please add appropriate labels:
|
||||||
|
|
||||||
|
### Deployment Type (Required)
|
||||||
|
Always specify where you encountered the issue:
|
||||||
|
- `cloud` - For issues on SurfSense Cloud (hosted version)
|
||||||
|
- `self-hosted` - For issues on self-hosted installations
|
||||||
|
|
||||||
|
### Priority (Optional but Recommended)
|
||||||
|
- `critical` - System crashes, data loss, security issues
|
||||||
|
- `high` - Major functionality broken
|
||||||
|
- `medium` - Feature partially works or has workarounds
|
||||||
|
- `low` - Minor issues, cosmetic problems
|
||||||
|
|
||||||
|
### Component (Optional)
|
||||||
|
Help us route your issue faster:
|
||||||
|
- `backend` - Python/FastAPI backend issues
|
||||||
|
- `frontend` - Web interface issues
|
||||||
|
- `extension` - Browser extension issues
|
||||||
|
- `database` - Database-related issues
|
||||||
|
- `api` - API endpoint issues
|
||||||
|
- `ui/ux` - User interface/experience issues
|
||||||
|
|
||||||
|
## ✅ Before Creating an Issue
|
||||||
|
|
||||||
|
1. **Search existing issues** - Your issue might already be reported
|
||||||
|
2. **Check documentation** - The answer might be in our docs
|
||||||
|
3. **Provide complete information** - Fill out all required fields in the template
|
||||||
|
4. **Be specific** - Include version numbers, error messages, and reproduction steps
|
||||||
|
|
||||||
|
## 💡 Tips for Quality Issues
|
||||||
|
|
||||||
|
### For Bug Reports
|
||||||
|
- Include screenshots or videos when possible
|
||||||
|
- Provide complete error messages and logs
|
||||||
|
- Specify exact steps to reproduce
|
||||||
|
- Include environment details (browser, OS, versions)
|
||||||
|
|
||||||
|
### For Feature Requests
|
||||||
|
- Explain the use case and benefits
|
||||||
|
- Describe the problem it solves
|
||||||
|
- Consider implementation challenges
|
||||||
|
|
||||||
|
## 🚀 Self-Hosted Users
|
||||||
|
|
||||||
|
If you're using a self-hosted version, please include:
|
||||||
|
- Installation method (Docker, manual, etc.)
|
||||||
|
- Python and Node.js versions
|
||||||
|
- Database type and version
|
||||||
|
- Any custom configuration
|
||||||
|
|
||||||
|
## 📞 Need Help?
|
||||||
|
|
||||||
|
- **Questions?** Use [GitHub Discussions](https://github.com/SurfSense/SurfSense/discussions)
|
||||||
|
- **Security Issues?** Please report privately to the maintainers
|
||||||
|
- **General Chat?** Join our community channels
|
||||||
|
|
||||||
|
Thank you for helping make SurfSense better! 🎉
|
||||||
|
|
||||||
59
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
59
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
---
|
||||||
|
name: Bug Report
|
||||||
|
about: Report a bug or unexpected behavior in SurfSense
|
||||||
|
title: '[BUG] '
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug Description
|
||||||
|
<!-- Provide a clear and concise description of the bug -->
|
||||||
|
|
||||||
|
## Deployment Type
|
||||||
|
<!-- Please select where you encountered this issue -->
|
||||||
|
- [ ] SurfSense Cloud (hosted version)
|
||||||
|
- [ ] Self-hosted version
|
||||||
|
|
||||||
|
## Steps to Reproduce
|
||||||
|
<!-- Provide detailed steps to reproduce the behavior -->
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '...'
|
||||||
|
3. Scroll down to '...'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
<!-- Describe what you expected to happen -->
|
||||||
|
|
||||||
|
## Actual Behavior
|
||||||
|
<!-- Describe what actually happened -->
|
||||||
|
|
||||||
|
## Screenshots/Videos
|
||||||
|
<!-- If applicable, add screenshots or videos to help explain the problem -->
|
||||||
|
|
||||||
|
## Environment Information
|
||||||
|
<!-- Please complete the following information -->
|
||||||
|
- **Browser:** [e.g., Chrome 120, Firefox 121, Safari 17]
|
||||||
|
- **Operating System:** [e.g., Windows 11, macOS 14, Ubuntu 22.04]
|
||||||
|
- **SurfSense Version:** [e.g., v1.0.0 or commit hash if self-hosted]
|
||||||
|
|
||||||
|
### Additional Environment Details (for Self-hosted only)
|
||||||
|
<!-- If you selected self-hosted, please provide: -->
|
||||||
|
- **Python Version:** [e.g., 3.11]
|
||||||
|
- **Node.js Version:** [e.g., 20.10.0]
|
||||||
|
- **Database:** [e.g., PostgreSQL 15]
|
||||||
|
- **Deployment Method:** [e.g., Docker, Manual installation]
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
<!-- Add any other context about the problem here -->
|
||||||
|
|
||||||
|
## Logs/Error Messages
|
||||||
|
<!-- If applicable, paste relevant logs or error messages -->
|
||||||
|
```
|
||||||
|
Paste logs here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] I have searched existing issues to ensure this is not a duplicate
|
||||||
|
- [ ] I have provided all the required information above
|
||||||
|
- [ ] I have added appropriate labels (bug, deployment type)
|
||||||
|
|
||||||
12
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
12
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: 📚 Documentation
|
||||||
|
url: https://github.com/SurfSense/SurfSense/tree/main/docs
|
||||||
|
about: Check our documentation for guides and setup instructions
|
||||||
|
- name: 💬 Discussions
|
||||||
|
url: https://github.com/SurfSense/SurfSense/discussions
|
||||||
|
about: Ask questions and discuss ideas with the community
|
||||||
|
- name: 🤝 Contributing Guide
|
||||||
|
url: https://github.com/SurfSense/SurfSense/blob/main/CONTRIBUTING.md
|
||||||
|
about: Learn how to contribute to SurfSense
|
||||||
|
|
||||||
41
.github/ISSUE_TEMPLATE/documentation.md
vendored
Normal file
41
.github/ISSUE_TEMPLATE/documentation.md
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
---
|
||||||
|
name: Documentation Issue
|
||||||
|
about: Report issues or suggest improvements for documentation
|
||||||
|
title: '[DOCS] '
|
||||||
|
labels: documentation
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation Issue
|
||||||
|
<!-- Describe the documentation issue or improvement needed -->
|
||||||
|
|
||||||
|
## Location
|
||||||
|
<!-- Where is the documentation issue? -->
|
||||||
|
- **File/Page:** [e.g., README.md, docs/setup.md]
|
||||||
|
- **Section:** [e.g., Installation, Configuration]
|
||||||
|
- **URL:** [if applicable]
|
||||||
|
|
||||||
|
## Issue Type
|
||||||
|
<!-- Select the type of documentation issue -->
|
||||||
|
- [ ] Outdated information
|
||||||
|
- [ ] Missing information
|
||||||
|
- [ ] Incorrect information
|
||||||
|
- [ ] Typo/Grammar
|
||||||
|
- [ ] Unclear explanation
|
||||||
|
- [ ] Missing example
|
||||||
|
- [ ] Other (specify below)
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
<!-- Describe what the documentation currently says (if applicable) -->
|
||||||
|
|
||||||
|
## Suggested Improvement
|
||||||
|
<!-- Describe what you think should be changed or added -->
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
<!-- Add any other context, screenshots, or examples here -->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] I have checked that this documentation issue doesn't already exist
|
||||||
|
- [ ] I have provided clear location information
|
||||||
|
- [ ] I have added appropriate labels (documentation)
|
||||||
|
|
||||||
52
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
52
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
---
|
||||||
|
name: Feature Request
|
||||||
|
about: Suggest a new feature or enhancement for SurfSense
|
||||||
|
title: '[FEATURE] '
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature Description
|
||||||
|
<!-- Provide a clear and concise description of the feature you'd like to see -->
|
||||||
|
|
||||||
|
## Target Deployment
|
||||||
|
<!-- Please select which deployment(s) this feature should apply to -->
|
||||||
|
- [ ] SurfSense Cloud (hosted version)
|
||||||
|
- [ ] Self-hosted version
|
||||||
|
- [ ] Both
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
<!-- Describe the problem or use case this feature would solve -->
|
||||||
|
<!-- Example: "I'm always frustrated when..." -->
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
<!-- Describe how you envision this feature working -->
|
||||||
|
|
||||||
|
## Alternative Solutions
|
||||||
|
<!-- Describe any alternative solutions or features you've considered -->
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
<!-- Explain how this feature would benefit SurfSense users -->
|
||||||
|
-
|
||||||
|
-
|
||||||
|
|
||||||
|
## Use Case Examples
|
||||||
|
<!-- Provide concrete examples of how this feature would be used -->
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
<!-- Add any other context, mockups, or screenshots about the feature request here -->
|
||||||
|
|
||||||
|
## Implementation Considerations
|
||||||
|
<!-- Optional: If you have technical insights, share them here -->
|
||||||
|
- [ ] This may require frontend changes
|
||||||
|
- [ ] This may require backend changes
|
||||||
|
- [ ] This may require database changes
|
||||||
|
- [ ] This may affect existing features
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] I have searched existing issues/feature requests to ensure this is not a duplicate
|
||||||
|
- [ ] I have provided a clear description of the feature
|
||||||
|
- [ ] I have added appropriate labels (enhancement, deployment type)
|
||||||
|
|
||||||
15
README.md
15
README.md
|
|
@ -141,19 +141,24 @@ Check out our public roadmap and contribute your ideas or feedback:
|
||||||
|
|
||||||
### Installation Options
|
### Installation Options
|
||||||
|
|
||||||
SurfSense provides two installation methods:
|
SurfSense provides three options to get started:
|
||||||
|
|
||||||
1. **[Docker Installation](https://www.surfsense.net/docs/docker-installation)** - The easiest way to get SurfSense up and running with all dependencies containerized.
|
1. **[SurfSense Cloud](https://www.surfsense.com/login)** - The easiest way to try SurfSense without any setup.
|
||||||
|
- No installation required
|
||||||
|
- Instant access to all features
|
||||||
|
- Perfect for getting started quickly
|
||||||
|
|
||||||
|
2. **[Docker Installation (Recommended for Self-Hosting)](https://www.surfsense.net/docs/docker-installation)** - Easy way to get SurfSense up and running with all dependencies containerized.
|
||||||
- Includes pgAdmin for database management through a web UI
|
- Includes pgAdmin for database management through a web UI
|
||||||
- Supports environment variable customization via `.env` file
|
- Supports environment variable customization via `.env` file
|
||||||
- Flexible deployment options (full stack or core services only)
|
- Flexible deployment options (full stack or core services only)
|
||||||
- No need to manually edit configuration files between environments
|
- No need to manually edit configuration files between environments
|
||||||
|
|
||||||
2. **[Manual Installation (Recommended)](https://www.surfsense.net/docs/manual-installation)** - For users who prefer more control over their setup or need to customize their deployment.
|
3. **[Manual Installation](https://www.surfsense.net/docs/manual-installation)** - For users who prefer more control over their setup or need to customize their deployment.
|
||||||
|
|
||||||
Both installation guides include detailed OS-specific instructions for Windows, macOS, and Linux.
|
Docker and manual installation guides include detailed OS-specific instructions for Windows, macOS, and Linux.
|
||||||
|
|
||||||
Before installation, make sure to complete the [prerequisite setup steps](https://www.surfsense.net/docs/) including:
|
Before self-hosting installation, make sure to complete the [prerequisite setup steps](https://www.surfsense.net/docs/) including:
|
||||||
- Auth setup
|
- Auth setup
|
||||||
- **File Processing ETL Service** (choose one):
|
- **File Processing ETL Service** (choose one):
|
||||||
- Unstructured.io API key (supports 34+ formats)
|
- Unstructured.io API key (supports 34+ formats)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ from fastapi_users import schemas
|
||||||
|
|
||||||
|
|
||||||
class UserRead(schemas.BaseUser[uuid.UUID]):
|
class UserRead(schemas.BaseUser[uuid.UUID]):
|
||||||
pass
|
pages_limit: int
|
||||||
|
pages_used: int
|
||||||
|
|
||||||
|
|
||||||
class UserCreate(schemas.BaseUserCreate):
|
class UserCreate(schemas.BaseUserCreate):
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { AnnouncementBanner } from "@/components/announcement-banner";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
||||||
interface DashboardLayoutProps {
|
interface DashboardLayoutProps {
|
||||||
|
|
@ -40,5 +41,10 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
return (
|
||||||
|
<>
|
||||||
|
<AnnouncementBanner />
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Logo } from "@/components/Logo";
|
import { Logo } from "@/components/Logo";
|
||||||
import { ThemeTogglerComponent } from "@/components/theme/theme-toggle";
|
import { ThemeTogglerComponent } from "@/components/theme/theme-toggle";
|
||||||
|
|
@ -34,16 +33,8 @@ import {
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Spotlight } from "@/components/ui/spotlight";
|
import { Spotlight } from "@/components/ui/spotlight";
|
||||||
import { Tilt } from "@/components/ui/tilt";
|
import { Tilt } from "@/components/ui/tilt";
|
||||||
|
import { useUser } from "@/hooks";
|
||||||
import { useSearchSpaces } from "@/hooks/use-search-spaces";
|
import { useSearchSpaces } from "@/hooks/use-search-spaces";
|
||||||
import { apiClient } from "@/lib/api";
|
|
||||||
|
|
||||||
interface User {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
is_active: boolean;
|
|
||||||
is_superuser: boolean;
|
|
||||||
is_verified: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a date string into a readable format
|
* Formats a date string into a readable format
|
||||||
|
|
@ -163,35 +154,8 @@ const DashboardPage = () => {
|
||||||
|
|
||||||
const { searchSpaces, loading, error, refreshSearchSpaces } = useSearchSpaces();
|
const { searchSpaces, loading, error, refreshSearchSpaces } = useSearchSpaces();
|
||||||
|
|
||||||
// User state management
|
|
||||||
const [user, setUser] = useState<User | null>(null);
|
|
||||||
const [isLoadingUser, setIsLoadingUser] = useState(true);
|
|
||||||
const [userError, setUserError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// Fetch user details
|
// Fetch user details
|
||||||
useEffect(() => {
|
const { user, loading: isLoadingUser, error: userError } = useUser();
|
||||||
const fetchUser = async () => {
|
|
||||||
try {
|
|
||||||
if (typeof window === "undefined") return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const userData = await apiClient.get<User>("users/me");
|
|
||||||
setUser(userData);
|
|
||||||
setUserError(null);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching user:", error);
|
|
||||||
setUserError(error instanceof Error ? error.message : "Unknown error occurred");
|
|
||||||
} finally {
|
|
||||||
setIsLoadingUser(false);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error in fetchUser:", error);
|
|
||||||
setIsLoadingUser(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchUser();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Create user object for UserDropdown
|
// Create user object for UserDropdown
|
||||||
const customUser = {
|
const customUser = {
|
||||||
|
|
|
||||||
42
surfsense_web/components/announcement-banner.tsx
Normal file
42
surfsense_web/components/announcement-banner.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ExternalLink, Info, X } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
export function AnnouncementBanner() {
|
||||||
|
const [isVisible, setIsVisible] = useState(true);
|
||||||
|
|
||||||
|
if (!isVisible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative bg-gradient-to-r from-blue-600 to-blue-500 dark:from-blue-700 dark:to-blue-600 border-b border-blue-700 dark:border-blue-800">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<div className="flex items-center justify-center gap-3 py-2.5">
|
||||||
|
<Info className="h-4 w-4 text-blue-50 flex-shrink-0" />
|
||||||
|
<p className="text-sm text-blue-50 text-center font-medium">
|
||||||
|
SurfSense is a work in progress.{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/MODSetter/SurfSense/issues"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-1 underline decoration-blue-200 underline-offset-2 hover:decoration-white transition-colors"
|
||||||
|
>
|
||||||
|
Report issues on GitHub
|
||||||
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 w-7 p-0 shrink-0 text-blue-100 hover:text-white hover:bg-blue-700/50 dark:hover:bg-blue-800/50 absolute right-4"
|
||||||
|
onClick={() => setIsVisible(false)}
|
||||||
|
>
|
||||||
|
<X className="h-3.5 w-3.5" />
|
||||||
|
<span className="sr-only">Dismiss</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -13,24 +13,7 @@ import {
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { apiClient } from "@/lib/api";
|
import { useChats, useSearchSpace, useUser } from "@/hooks";
|
||||||
|
|
||||||
interface Chat {
|
|
||||||
created_at: string;
|
|
||||||
id: number;
|
|
||||||
type: string;
|
|
||||||
title: string;
|
|
||||||
messages: string[];
|
|
||||||
search_space_id: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SearchSpace {
|
|
||||||
created_at: string;
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
user_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AppSidebarProviderProps {
|
interface AppSidebarProviderProps {
|
||||||
searchSpaceId: string;
|
searchSpaceId: string;
|
||||||
|
|
@ -58,21 +41,25 @@ export function AppSidebarProvider({
|
||||||
}: AppSidebarProviderProps) {
|
}: AppSidebarProviderProps) {
|
||||||
const t = useTranslations("dashboard");
|
const t = useTranslations("dashboard");
|
||||||
const tCommon = useTranslations("common");
|
const tCommon = useTranslations("common");
|
||||||
const [recentChats, setRecentChats] = useState<
|
|
||||||
{
|
// Use the new hooks
|
||||||
name: string;
|
const {
|
||||||
url: string;
|
chats,
|
||||||
icon: string;
|
loading: isLoadingChats,
|
||||||
id: number;
|
error: chatError,
|
||||||
search_space_id: number;
|
fetchChats: fetchRecentChats,
|
||||||
actions: { name: string; icon: string; onClick: () => void }[];
|
deleteChat,
|
||||||
}[]
|
} = useChats({ searchSpaceId, limit: 5, skip: 0 });
|
||||||
>([]);
|
|
||||||
const [searchSpace, setSearchSpace] = useState<SearchSpace | null>(null);
|
const {
|
||||||
const [isLoadingChats, setIsLoadingChats] = useState(true);
|
searchSpace,
|
||||||
const [isLoadingSearchSpace, setIsLoadingSearchSpace] = useState(true);
|
loading: isLoadingSearchSpace,
|
||||||
const [chatError, setChatError] = useState<string | null>(null);
|
error: searchSpaceError,
|
||||||
const [searchSpaceError, setSearchSpaceError] = useState<string | null>(null);
|
fetchSearchSpace,
|
||||||
|
} = useSearchSpace({ searchSpaceId });
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
const [chatToDelete, setChatToDelete] = useState<{ id: number; name: string } | null>(null);
|
const [chatToDelete, setChatToDelete] = useState<{ id: number; name: string } | null>(null);
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
|
@ -83,95 +70,32 @@ export function AppSidebarProvider({
|
||||||
setIsClient(true);
|
setIsClient(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Memoized fetch function for chats
|
|
||||||
const fetchRecentChats = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
// Only run on client-side
|
|
||||||
if (typeof window === "undefined") return;
|
|
||||||
|
|
||||||
const chats: Chat[] = await apiClient.get<Chat[]>(
|
|
||||||
`api/v1/chats?limit=5&skip=0&search_space_id=${searchSpaceId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sort chats by created_at in descending order (newest first)
|
|
||||||
const sortedChats = chats.sort(
|
|
||||||
(a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Transform API response to the format expected by AppSidebar
|
|
||||||
const formattedChats = sortedChats.map((chat) => ({
|
|
||||||
name: chat.title || `Chat ${chat.id}`,
|
|
||||||
url: `/dashboard/${chat.search_space_id}/researcher/${chat.id}`,
|
|
||||||
icon: "MessageCircleMore",
|
|
||||||
id: chat.id,
|
|
||||||
search_space_id: chat.search_space_id,
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
name: "Delete",
|
|
||||||
icon: "Trash2",
|
|
||||||
onClick: () => {
|
|
||||||
setChatToDelete({ id: chat.id, name: chat.title || `Chat ${chat.id}` });
|
|
||||||
setShowDeleteDialog(true);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
|
|
||||||
setRecentChats(formattedChats);
|
|
||||||
setChatError(null);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching chats:", error);
|
|
||||||
setChatError(error instanceof Error ? error.message : "Unknown error occurred");
|
|
||||||
setRecentChats([]);
|
|
||||||
} finally {
|
|
||||||
setIsLoadingChats(false);
|
|
||||||
}
|
|
||||||
}, [searchSpaceId]);
|
|
||||||
|
|
||||||
// Memoized fetch function for search space
|
|
||||||
const fetchSearchSpace = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
// Only run on client-side
|
|
||||||
if (typeof window === "undefined") return;
|
|
||||||
|
|
||||||
const data: SearchSpace = await apiClient.get<SearchSpace>(
|
|
||||||
`api/v1/searchspaces/${searchSpaceId}`
|
|
||||||
);
|
|
||||||
setSearchSpace(data);
|
|
||||||
setSearchSpaceError(null);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching search space:", error);
|
|
||||||
setSearchSpaceError(error instanceof Error ? error.message : "Unknown error occurred");
|
|
||||||
} finally {
|
|
||||||
setIsLoadingSearchSpace(false);
|
|
||||||
}
|
|
||||||
}, [searchSpaceId]);
|
|
||||||
|
|
||||||
// Retry function
|
// Retry function
|
||||||
const retryFetch = useCallback(() => {
|
const retryFetch = useCallback(() => {
|
||||||
setChatError(null);
|
|
||||||
setSearchSpaceError(null);
|
|
||||||
setIsLoadingChats(true);
|
|
||||||
setIsLoadingSearchSpace(true);
|
|
||||||
fetchRecentChats();
|
fetchRecentChats();
|
||||||
fetchSearchSpace();
|
fetchSearchSpace();
|
||||||
}, [fetchRecentChats, fetchSearchSpace]);
|
}, [fetchRecentChats, fetchSearchSpace]);
|
||||||
|
|
||||||
// Fetch recent chats
|
// Transform API response to the format expected by AppSidebar
|
||||||
useEffect(() => {
|
const recentChats = useMemo(() => {
|
||||||
fetchRecentChats();
|
return chats.map((chat) => ({
|
||||||
|
name: chat.title || `Chat ${chat.id}`,
|
||||||
// Set up a refresh interval (every 5 minutes)
|
url: `/dashboard/${chat.search_space_id}/researcher/${chat.id}`,
|
||||||
const intervalId = setInterval(fetchRecentChats, 5 * 60 * 1000);
|
icon: "MessageCircleMore",
|
||||||
|
id: chat.id,
|
||||||
// Clean up interval on component unmount
|
search_space_id: chat.search_space_id,
|
||||||
return () => clearInterval(intervalId);
|
actions: [
|
||||||
}, [fetchRecentChats]);
|
{
|
||||||
|
name: "Delete",
|
||||||
// Fetch search space details
|
icon: "Trash2",
|
||||||
useEffect(() => {
|
onClick: () => {
|
||||||
fetchSearchSpace();
|
setChatToDelete({ id: chat.id, name: chat.title || `Chat ${chat.id}` });
|
||||||
}, [fetchSearchSpace]);
|
setShowDeleteDialog(true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
}, [chats]);
|
||||||
|
|
||||||
// Handle delete chat with better error handling
|
// Handle delete chat with better error handling
|
||||||
const handleDeleteChat = useCallback(async () => {
|
const handleDeleteChat = useCallback(async () => {
|
||||||
|
|
@ -179,11 +103,7 @@ export function AppSidebarProvider({
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
|
await deleteChat(chatToDelete.id);
|
||||||
await apiClient.delete(`api/v1/chats/${chatToDelete.id}`);
|
|
||||||
|
|
||||||
// Update local state
|
|
||||||
setRecentChats((prev) => prev.filter((chat) => chat.id !== chatToDelete.id));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting chat:", error);
|
console.error("Error deleting chat:", error);
|
||||||
// You could show a toast notification here
|
// You could show a toast notification here
|
||||||
|
|
@ -192,7 +112,7 @@ export function AppSidebarProvider({
|
||||||
setShowDeleteDialog(false);
|
setShowDeleteDialog(false);
|
||||||
setChatToDelete(null);
|
setChatToDelete(null);
|
||||||
}
|
}
|
||||||
}, [chatToDelete]);
|
}, [chatToDelete, deleteChat]);
|
||||||
|
|
||||||
// Memoized fallback chats
|
// Memoized fallback chats
|
||||||
const fallbackChats = useMemo(() => {
|
const fallbackChats = useMemo(() => {
|
||||||
|
|
@ -260,14 +180,34 @@ export function AppSidebarProvider({
|
||||||
tCommon,
|
tCommon,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Prepare page usage data
|
||||||
|
const pageUsage = user
|
||||||
|
? {
|
||||||
|
pagesUsed: user.pages_used,
|
||||||
|
pagesLimit: user.pages_limit,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
// Show loading state if not client-side
|
// Show loading state if not client-side
|
||||||
if (!isClient) {
|
if (!isClient) {
|
||||||
return <AppSidebar navSecondary={navSecondary} navMain={navMain} RecentChats={[]} />;
|
return (
|
||||||
|
<AppSidebar
|
||||||
|
navSecondary={navSecondary}
|
||||||
|
navMain={navMain}
|
||||||
|
RecentChats={[]}
|
||||||
|
pageUsage={pageUsage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppSidebar navSecondary={updatedNavSecondary} navMain={navMain} RecentChats={displayChats} />
|
<AppSidebar
|
||||||
|
navSecondary={updatedNavSecondary}
|
||||||
|
navMain={navMain}
|
||||||
|
RecentChats={displayChats}
|
||||||
|
pageUsage={pageUsage}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Delete Confirmation Dialog */}
|
{/* Delete Confirmation Dialog */}
|
||||||
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import { Logo } from "@/components/Logo";
|
||||||
import { NavMain } from "@/components/sidebar/nav-main";
|
import { NavMain } from "@/components/sidebar/nav-main";
|
||||||
import { NavProjects } from "@/components/sidebar/nav-projects";
|
import { NavProjects } from "@/components/sidebar/nav-projects";
|
||||||
import { NavSecondary } from "@/components/sidebar/nav-secondary";
|
import { NavSecondary } from "@/components/sidebar/nav-secondary";
|
||||||
|
import { PageUsageDisplay } from "@/components/sidebar/page-usage-display";
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
|
|
@ -175,6 +176,10 @@ interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
|
||||||
email: string;
|
email: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
};
|
};
|
||||||
|
pageUsage?: {
|
||||||
|
pagesUsed: number;
|
||||||
|
pagesLimit: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memoized AppSidebar component for better performance
|
// Memoized AppSidebar component for better performance
|
||||||
|
|
@ -182,6 +187,7 @@ export const AppSidebar = memo(function AppSidebar({
|
||||||
navMain = defaultData.navMain,
|
navMain = defaultData.navMain,
|
||||||
navSecondary = defaultData.navSecondary,
|
navSecondary = defaultData.navSecondary,
|
||||||
RecentChats = defaultData.RecentChats,
|
RecentChats = defaultData.RecentChats,
|
||||||
|
pageUsage,
|
||||||
...props
|
...props
|
||||||
}: AppSidebarProps) {
|
}: AppSidebarProps) {
|
||||||
// Process navMain to resolve icon names to components
|
// Process navMain to resolve icon names to components
|
||||||
|
|
@ -246,6 +252,9 @@ export const AppSidebar = memo(function AppSidebar({
|
||||||
)}
|
)}
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
|
{pageUsage && (
|
||||||
|
<PageUsageDisplay pagesUsed={pageUsage.pagesUsed} pagesLimit={pageUsage.pagesLimit} />
|
||||||
|
)}
|
||||||
<NavSecondary items={processedNavSecondary} className="mt-auto" />
|
<NavSecondary items={processedNavSecondary} className="mt-auto" />
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
|
|
||||||
63
surfsense_web/components/sidebar/page-usage-display.tsx
Normal file
63
surfsense_web/components/sidebar/page-usage-display.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Mail } from "lucide-react";
|
||||||
|
import { Progress } from "@/components/ui/progress";
|
||||||
|
import {
|
||||||
|
SidebarGroup,
|
||||||
|
SidebarGroupContent,
|
||||||
|
SidebarGroupLabel,
|
||||||
|
useSidebar,
|
||||||
|
} from "@/components/ui/sidebar";
|
||||||
|
|
||||||
|
interface PageUsageDisplayProps {
|
||||||
|
pagesUsed: number;
|
||||||
|
pagesLimit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PageUsageDisplay({ pagesUsed, pagesLimit }: PageUsageDisplayProps) {
|
||||||
|
const { state } = useSidebar();
|
||||||
|
const usagePercentage = (pagesUsed / pagesLimit) * 100;
|
||||||
|
const isCollapsed = state === "collapsed";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarGroup>
|
||||||
|
<SidebarGroupLabel className="group-data-[collapsible=icon]:hidden">
|
||||||
|
Page Usage
|
||||||
|
</SidebarGroupLabel>
|
||||||
|
<SidebarGroupContent>
|
||||||
|
<div className="space-y-2 px-2 py-2">
|
||||||
|
{isCollapsed ? (
|
||||||
|
// Show only a compact progress indicator when collapsed
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Progress value={usagePercentage} className="h-2 w-8" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// Show full details when expanded
|
||||||
|
<>
|
||||||
|
<div className="flex justify-between items-center text-xs">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{pagesUsed.toLocaleString()} / {pagesLimit.toLocaleString()} pages
|
||||||
|
</span>
|
||||||
|
<span className="font-medium">{usagePercentage.toFixed(0)}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={usagePercentage} className="h-2" />
|
||||||
|
<div className="flex items-start gap-2 pt-1">
|
||||||
|
<Mail className="h-3 w-3 text-muted-foreground mt-0.5 flex-shrink-0" />
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-tight">
|
||||||
|
Contact{" "}
|
||||||
|
<a
|
||||||
|
href="mailto:rohan@surfsense.com"
|
||||||
|
className="text-primary hover:underline font-medium"
|
||||||
|
>
|
||||||
|
rohan@surfsense.com
|
||||||
|
</a>{" "}
|
||||||
|
to increase limits
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
export * from "./use-chats";
|
||||||
export * from "./use-document-by-chunk";
|
export * from "./use-document-by-chunk";
|
||||||
export * from "./use-logs";
|
export * from "./use-logs";
|
||||||
export * from "./use-search-source-connectors";
|
export * from "./use-search-source-connectors";
|
||||||
|
export * from "./use-search-space";
|
||||||
|
export * from "./use-user";
|
||||||
|
|
|
||||||
124
surfsense_web/hooks/use-chats.ts
Normal file
124
surfsense_web/hooks/use-chats.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Chat {
|
||||||
|
created_at: string;
|
||||||
|
id: number;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
messages: string[];
|
||||||
|
search_space_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseChatsOptions {
|
||||||
|
searchSpaceId: string | number;
|
||||||
|
limit?: number;
|
||||||
|
skip?: number;
|
||||||
|
autoFetch?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useChats({
|
||||||
|
searchSpaceId,
|
||||||
|
limit = 5,
|
||||||
|
skip = 0,
|
||||||
|
autoFetch = true,
|
||||||
|
}: UseChatsOptions) {
|
||||||
|
const [chats, setChats] = useState<Chat[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchChats = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
// Only run on client-side
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats?limit=${limit}&skip=${skip}&search_space_id=${searchSpaceId}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`,
|
||||||
|
},
|
||||||
|
method: "GET",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
// Clear token and redirect to home
|
||||||
|
localStorage.removeItem("surfsense_bearer_token");
|
||||||
|
window.location.href = "/";
|
||||||
|
throw new Error("Unauthorized: Redirecting to login page");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch chats: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Sort chats by created_at in descending order (newest first)
|
||||||
|
const sortedChats = data.sort(
|
||||||
|
(a: Chat, b: Chat) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
||||||
|
);
|
||||||
|
|
||||||
|
setChats(sortedChats);
|
||||||
|
setError(null);
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message || "Failed to fetch chats");
|
||||||
|
console.error("Error fetching chats:", err);
|
||||||
|
setChats([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [searchSpaceId, limit, skip]);
|
||||||
|
|
||||||
|
const deleteChat = useCallback(async (chatId: number) => {
|
||||||
|
try {
|
||||||
|
// Only run on client-side
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${chatId}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`,
|
||||||
|
},
|
||||||
|
method: "DELETE",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
// Clear token and redirect to home
|
||||||
|
localStorage.removeItem("surfsense_bearer_token");
|
||||||
|
window.location.href = "/";
|
||||||
|
throw new Error("Unauthorized: Redirecting to login page");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to delete chat: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update local state to remove the deleted chat
|
||||||
|
setChats((prev) => prev.filter((chat) => chat.id !== chatId));
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("Error deleting chat:", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoFetch) {
|
||||||
|
fetchChats();
|
||||||
|
|
||||||
|
// Set up a refresh interval (every 5 minutes)
|
||||||
|
const intervalId = setInterval(fetchChats, 5 * 60 * 1000);
|
||||||
|
|
||||||
|
// Clean up interval on component unmount
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}
|
||||||
|
}, [autoFetch, fetchChats]);
|
||||||
|
|
||||||
|
return { chats, loading, error, fetchChats, deleteChat };
|
||||||
|
}
|
||||||
69
surfsense_web/hooks/use-search-space.ts
Normal file
69
surfsense_web/hooks/use-search-space.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface SearchSpace {
|
||||||
|
created_at: string;
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
user_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseSearchSpaceOptions {
|
||||||
|
searchSpaceId: string | number;
|
||||||
|
autoFetch?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSearchSpace({ searchSpaceId, autoFetch = true }: UseSearchSpaceOptions) {
|
||||||
|
const [searchSpace, setSearchSpace] = useState<SearchSpace | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchSearchSpace = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
// Only run on client-side
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`,
|
||||||
|
},
|
||||||
|
method: "GET",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
// Clear token and redirect to home
|
||||||
|
localStorage.removeItem("surfsense_bearer_token");
|
||||||
|
window.location.href = "/";
|
||||||
|
throw new Error("Unauthorized: Redirecting to login page");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch search space: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
setSearchSpace(data);
|
||||||
|
setError(null);
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message || "Failed to fetch search space");
|
||||||
|
console.error("Error fetching search space:", err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [searchSpaceId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoFetch) {
|
||||||
|
fetchSearchSpace();
|
||||||
|
}
|
||||||
|
}, [autoFetch, fetchSearchSpace]);
|
||||||
|
|
||||||
|
return { searchSpace, loading, error, fetchSearchSpace };
|
||||||
|
}
|
||||||
61
surfsense_web/hooks/use-user.ts
Normal file
61
surfsense_web/hooks/use-user.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
is_active: boolean;
|
||||||
|
is_superuser: boolean;
|
||||||
|
is_verified: boolean;
|
||||||
|
pages_limit: number;
|
||||||
|
pages_used: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUser() {
|
||||||
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUser = async () => {
|
||||||
|
try {
|
||||||
|
// Only run on client-side
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/users/me`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`,
|
||||||
|
},
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
// Clear token and redirect to home
|
||||||
|
localStorage.removeItem("surfsense_bearer_token");
|
||||||
|
window.location.href = "/";
|
||||||
|
throw new Error("Unauthorized: Redirecting to login page");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch user: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
setUser(data);
|
||||||
|
setError(null);
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message || "Failed to fetch user");
|
||||||
|
console.error("Error fetching user:", err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUser();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { user, loading, error };
|
||||||
|
}
|
||||||
|
|
@ -1,170 +0,0 @@
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom fetch wrapper that handles authentication and redirects to home page on 401 Unauthorized
|
|
||||||
*
|
|
||||||
* @param url - The URL to fetch
|
|
||||||
* @param options - Fetch options
|
|
||||||
* @returns The fetch response
|
|
||||||
*/
|
|
||||||
export async function fetchWithAuth(url: string, options: RequestInit = {}): Promise<Response> {
|
|
||||||
// Only run on client-side
|
|
||||||
if (typeof window === "undefined") {
|
|
||||||
return fetch(url, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get token from localStorage
|
|
||||||
const token = localStorage.getItem("surfsense_bearer_token");
|
|
||||||
|
|
||||||
// Add authorization header if token exists
|
|
||||||
const headers = {
|
|
||||||
...options.headers,
|
|
||||||
...(token && { Authorization: `Bearer ${token}` }),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Make the request
|
|
||||||
const response = await fetch(url, {
|
|
||||||
...options,
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle 401 Unauthorized response
|
|
||||||
if (response.status === 401) {
|
|
||||||
// Show error toast
|
|
||||||
toast.error("Session expired. Please log in again.");
|
|
||||||
|
|
||||||
// Clear token
|
|
||||||
localStorage.removeItem("surfsense_bearer_token");
|
|
||||||
|
|
||||||
// Redirect to home page
|
|
||||||
window.location.href = "/";
|
|
||||||
|
|
||||||
// Throw error to stop further processing
|
|
||||||
throw new Error("Unauthorized: Redirecting to login page");
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the full API URL
|
|
||||||
*
|
|
||||||
* @param path - The API path
|
|
||||||
* @returns The full API URL
|
|
||||||
*/
|
|
||||||
export function getApiUrl(path: string): string {
|
|
||||||
// Remove leading slash if present
|
|
||||||
const cleanPath = path.startsWith("/") ? path.slice(1) : path;
|
|
||||||
|
|
||||||
// Get backend URL from environment variable
|
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL;
|
|
||||||
|
|
||||||
if (!baseUrl) {
|
|
||||||
console.error("NEXT_PUBLIC_FASTAPI_BACKEND_URL is not defined");
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combine base URL and path
|
|
||||||
return `${baseUrl}/${cleanPath}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API client with methods for common operations
|
|
||||||
*/
|
|
||||||
export const apiClient = {
|
|
||||||
/**
|
|
||||||
* Make a GET request
|
|
||||||
*
|
|
||||||
* @param path - The API path
|
|
||||||
* @param options - Additional fetch options
|
|
||||||
* @returns The response data
|
|
||||||
*/
|
|
||||||
async get<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
||||||
const response = await fetchWithAuth(getApiUrl(path), {
|
|
||||||
method: "GET",
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json().catch(() => null);
|
|
||||||
throw new Error(`API error: ${response.status} ${errorData?.detail || response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make a POST request
|
|
||||||
*
|
|
||||||
* @param path - The API path
|
|
||||||
* @param data - The request body
|
|
||||||
* @param options - Additional fetch options
|
|
||||||
* @returns The response data
|
|
||||||
*/
|
|
||||||
async post<T>(path: string, data: any, options: RequestInit = {}): Promise<T> {
|
|
||||||
const response = await fetchWithAuth(getApiUrl(path), {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...options.headers,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json().catch(() => null);
|
|
||||||
throw new Error(`API error: ${response.status} ${errorData?.detail || response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make a PUT request
|
|
||||||
*
|
|
||||||
* @param path - The API path
|
|
||||||
* @param data - The request body
|
|
||||||
* @param options - Additional fetch options
|
|
||||||
* @returns The response data
|
|
||||||
*/
|
|
||||||
async put<T>(path: string, data: any, options: RequestInit = {}): Promise<T> {
|
|
||||||
const response = await fetchWithAuth(getApiUrl(path), {
|
|
||||||
method: "PUT",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...options.headers,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json().catch(() => null);
|
|
||||||
throw new Error(`API error: ${response.status} ${errorData?.detail || response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make a DELETE request
|
|
||||||
*
|
|
||||||
* @param path - The API path
|
|
||||||
* @param options - Additional fetch options
|
|
||||||
* @returns The response data
|
|
||||||
*/
|
|
||||||
async delete<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
||||||
const response = await fetchWithAuth(getApiUrl(path), {
|
|
||||||
method: "DELETE",
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json().catch(() => null);
|
|
||||||
throw new Error(`API error: ${response.status} ${errorData?.detail || response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue