mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-28 21:49:40 +02:00
Merge remote-tracking branch 'upstream/dev' into electon-desktop
This commit is contained in:
commit
b8a1d1f594
165 changed files with 17921 additions and 8767 deletions
|
|
@ -1,8 +1,14 @@
|
|||
import { loader } from "fumadocs-core/source";
|
||||
import type { Metadata } from "next";
|
||||
import { changelog } from "@/.source/server";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import { getMDXComponents } from "@/mdx-components";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Changelog | SurfSense",
|
||||
description: "See what's new in SurfSense.",
|
||||
};
|
||||
|
||||
const source = loader({
|
||||
baseUrl: "/changelog",
|
||||
source: changelog.toFumadocsSource(),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import React from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { ContactFormGridWithDetails } from "@/components/contact/contact-form";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Contact | SurfSense",
|
||||
description: "Get in touch with the SurfSense team.",
|
||||
};
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { AnimatePresence, motion } from "motion/react";
|
|||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { loginMutationAtom } from "@/atoms/auth/auth-mutation.atoms";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { getAuthErrorDetails, isNetworkError } from "@/lib/auth-errors";
|
||||
|
|
@ -25,15 +25,10 @@ export function LocalLoginForm() {
|
|||
title: null,
|
||||
message: null,
|
||||
});
|
||||
const [authType, setAuthType] = useState<string | null>(null);
|
||||
const authType = AUTH_TYPE;
|
||||
const router = useRouter();
|
||||
const [{ mutateAsync: login, isPending: isLoggingIn }] = useAtom(loginMutationAtom);
|
||||
|
||||
useEffect(() => {
|
||||
// Get the auth type from centralized config
|
||||
setAuthType(AUTH_TYPE);
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError({ title: null, message: null }); // Clear any previous errors
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import React from "react";
|
||||
import type { Metadata } from "next";
|
||||
import PricingBasic from "@/components/pricing/pricing-section";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Pricing | SurfSense",
|
||||
description: "Explore SurfSense plans and pricing options.",
|
||||
};
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -43,9 +43,12 @@ export default function RegisterPage() {
|
|||
}
|
||||
}, [router]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
submitForm();
|
||||
};
|
||||
|
||||
const submitForm = async () => {
|
||||
// Form validation
|
||||
if (password !== confirmPassword) {
|
||||
setError({ title: t("password_mismatch"), message: t("passwords_no_match_desc") });
|
||||
|
|
@ -140,7 +143,7 @@ export default function RegisterPage() {
|
|||
if (shouldRetry(errorCode)) {
|
||||
toastOptions.action = {
|
||||
label: tCommon("retry"),
|
||||
onClick: () => handleSubmit(e),
|
||||
onClick: () => submitForm(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export function DocumentTypeChip({ type, className }: { type: string; className?
|
|||
checkTruncation();
|
||||
window.addEventListener("resize", checkTruncation);
|
||||
return () => window.removeEventListener("resize", checkTruncation);
|
||||
}, []);
|
||||
}, [type]);
|
||||
|
||||
const chip = (
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { ListFilter, Search, Upload, X } from "lucide-react";
|
||||
import { FolderPlus, ListFilter, Search, Upload, X } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup";
|
||||
|
|
@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button";
|
|||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import type { DocumentTypeEnum } from "@/contracts/types/document.types";
|
||||
import { getDocumentTypeIcon, getDocumentTypeLabel } from "./DocumentTypeIcon";
|
||||
|
||||
|
|
@ -17,12 +18,14 @@ export function DocumentsFilters({
|
|||
searchValue,
|
||||
onToggleType,
|
||||
activeTypes,
|
||||
onCreateFolder,
|
||||
}: {
|
||||
typeCounts: Partial<Record<DocumentTypeEnum, number>>;
|
||||
onSearch: (v: string) => void;
|
||||
searchValue: string;
|
||||
onToggleType: (type: DocumentTypeEnum, checked: boolean) => void;
|
||||
activeTypes: DocumentTypeEnum[];
|
||||
onCreateFolder?: () => void;
|
||||
}) {
|
||||
const t = useTranslations("documents");
|
||||
const id = React.useId();
|
||||
|
|
@ -194,6 +197,23 @@ export function DocumentsFilters({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* New Folder Button */}
|
||||
{onCreateFolder && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-9 w-9 shrink-0 border-dashed border-sidebar-border text-sidebar-foreground/60 hover:text-sidebar-foreground hover:border-sidebar-border bg-sidebar"
|
||||
onClick={onCreateFolder}
|
||||
>
|
||||
<FolderPlus size={14} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>New folder</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Upload Button */}
|
||||
<Button
|
||||
data-joyride="upload-button"
|
||||
|
|
|
|||
|
|
@ -473,14 +473,14 @@ export function DocumentsTableShell({
|
|||
}, [deletableSelectedIds, bulkDeleteDocuments, deleteDocument]);
|
||||
|
||||
const bulkDeleteBar = hasDeletableSelection ? (
|
||||
<div className="flex items-center justify-center py-1.5 border-b border-border/50 bg-destructive/5 shrink-0 animate-in fade-in slide-in-from-top-1 duration-150">
|
||||
<div className="absolute inset-x-0 top-0 z-10 flex items-center justify-center py-1 pointer-events-none animate-in fade-in duration-150">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBulkDeleteConfirmOpen(true)}
|
||||
className="flex items-center gap-1.5 px-3 py-1 rounded-md bg-destructive text-destructive-foreground shadow-sm text-xs font-medium hover:bg-destructive/90 transition-colors"
|
||||
className="pointer-events-auto flex items-center gap-1.5 px-3 py-1 rounded-md bg-destructive text-destructive-foreground shadow-lg text-xs font-medium hover:bg-destructive/90 transition-colors"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
Delete ({deletableSelectedIds.length} selected)
|
||||
Delete {deletableSelectedIds.length} {deletableSelectedIds.length === 1 ? "item" : "items"}
|
||||
</button>
|
||||
</div>
|
||||
) : null;
|
||||
|
|
@ -526,7 +526,6 @@ export function DocumentsTableShell({
|
|||
</TableRow>
|
||||
</TableHeader>
|
||||
</Table>
|
||||
{bulkDeleteBar}
|
||||
{loading ? (
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table className="table-fixed w-full">
|
||||
|
|
@ -594,7 +593,8 @@ export function DocumentsTableShell({
|
|||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div ref={desktopScrollRef} className="flex-1 overflow-auto">
|
||||
<div ref={desktopScrollRef} className="flex-1 overflow-auto relative">
|
||||
{bulkDeleteBar}
|
||||
<Table className="table-fixed w-full">
|
||||
<TableBody>
|
||||
{sorted.map((doc) => {
|
||||
|
|
@ -788,9 +788,6 @@ export function DocumentsTableShell({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile bulk delete bar */}
|
||||
<div className="md:hidden">{bulkDeleteBar}</div>
|
||||
|
||||
{/* Mobile Card View */}
|
||||
{loading ? (
|
||||
<div className="md:hidden divide-y divide-border/50 flex-1 overflow-auto">
|
||||
|
|
@ -846,8 +843,9 @@ export function DocumentsTableShell({
|
|||
) : (
|
||||
<div
|
||||
ref={mobileScrollRef}
|
||||
className="md:hidden divide-y divide-border/50 flex-1 overflow-auto"
|
||||
className="md:hidden divide-y divide-border/50 flex-1 overflow-auto relative"
|
||||
>
|
||||
{bulkDeleteBar}
|
||||
{sorted.map((doc) => {
|
||||
const isMentioned = mentionedDocIds?.has(doc.id) ?? false;
|
||||
const statusState = doc.status?.state ?? "ready";
|
||||
|
|
|
|||
|
|
@ -24,8 +24,7 @@ import {
|
|||
} from "@/components/ui/dropdown-menu";
|
||||
import type { Document } from "./types";
|
||||
|
||||
// Only FILE and NOTE document types can be edited
|
||||
const EDITABLE_DOCUMENT_TYPES = ["FILE", "NOTE"] as const;
|
||||
const EDITABLE_DOCUMENT_TYPES = ["NOTE"] as const;
|
||||
|
||||
// SURFSENSE_DOCS are system-managed and cannot be deleted
|
||||
const NON_DELETABLE_DOCUMENT_TYPES = ["SURFSENSE_DOCS"] as const;
|
||||
|
|
@ -47,20 +46,14 @@ export function RowActions({
|
|||
document.document_type as (typeof EDITABLE_DOCUMENT_TYPES)[number]
|
||||
);
|
||||
|
||||
// Documents in "pending" or "processing" state should show disabled delete
|
||||
const isBeingProcessed =
|
||||
document.status?.state === "pending" || document.status?.state === "processing";
|
||||
|
||||
// FILE documents that failed processing cannot be edited
|
||||
const isFileFailed = document.document_type === "FILE" && document.status?.state === "failed";
|
||||
|
||||
// SURFSENSE_DOCS are system-managed and should not show delete at all
|
||||
const shouldShowDelete = !NON_DELETABLE_DOCUMENT_TYPES.includes(
|
||||
document.document_type as (typeof NON_DELETABLE_DOCUMENT_TYPES)[number]
|
||||
);
|
||||
|
||||
// Edit is disabled while processing OR for failed FILE documents
|
||||
const isEditDisabled = isBeingProcessed || isFileFailed;
|
||||
const isEditDisabled = isBeingProcessed;
|
||||
const isDeleteDisabled = isBeingProcessed;
|
||||
|
||||
const handleDelete = async () => {
|
||||
|
|
|
|||
133
surfsense_web/app/dashboard/[search_space_id]/logs/loading.tsx
Normal file
133
surfsense_web/app/dashboard/[search_space_id]/logs/loading.tsx
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
"use client";
|
||||
|
||||
import { motion } from "motion/react";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="w-full px-6 py-4 space-y-6 min-h-[calc(100vh-64px)]"
|
||||
>
|
||||
{/* Summary Dashboard Skeleton */}
|
||||
<motion.div
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
>
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<div key={i} className="rounded-lg border p-4">
|
||||
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-4 w-4 rounded-full" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-16" />
|
||||
<Skeleton className="h-3 w-32" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
{/* Header Section Skeleton */}
|
||||
<motion.div
|
||||
className="flex items-center justify-between"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<Skeleton className="h-4 w-64" />
|
||||
</div>
|
||||
<Skeleton className="h-9 w-24" />
|
||||
</motion.div>
|
||||
|
||||
{/* Filters Skeleton */}
|
||||
<motion.div
|
||||
className="flex flex-wrap items-center justify-start gap-3 w-full"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<div className="flex items-center gap-3 flex-wrap w-full sm:w-auto">
|
||||
<Skeleton className="h-9 w-full sm:w-60" />
|
||||
<Skeleton className="h-9 w-24" />
|
||||
<Skeleton className="h-9 w-24" />
|
||||
<Skeleton className="h-9 w-20" />
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Table Skeleton */}
|
||||
<motion.div
|
||||
className="rounded-md border overflow-hidden"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
>
|
||||
{/* Table Header */}
|
||||
<div className="border-b bg-muted/50 px-4 py-3 flex items-center gap-4">
|
||||
<Skeleton className="h-4 w-4" />
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-4 flex-1" />
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-4 w-8" />
|
||||
</div>
|
||||
|
||||
{/* Table Rows */}
|
||||
{[...Array(6)].map((_, i) => (
|
||||
<div key={i} className="border-b px-4 py-3 flex items-center gap-4 hover:bg-muted/50">
|
||||
<Skeleton className="h-4 w-4" />
|
||||
<Skeleton className="h-6 w-12 rounded-full" />
|
||||
<Skeleton className="h-6 w-16 rounded-full" />
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-4 w-4" />
|
||||
<Skeleton className="h-4 w-20" />
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<Skeleton className="h-3 w-48" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Skeleton className="h-3 w-24" />
|
||||
<Skeleton className="h-3 w-20" />
|
||||
</div>
|
||||
<Skeleton className="h-8 w-8" />
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
{/* Pagination Skeleton */}
|
||||
<div className="flex items-center justify-between gap-8 mt-4">
|
||||
<motion.div
|
||||
className="flex items-center gap-3"
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
>
|
||||
<Skeleton className="h-4 w-20 max-sm:sr-only" />
|
||||
<Skeleton className="h-9 w-16" />
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="flex grow justify-end"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<Skeleton className="h-4 w-40" />
|
||||
</motion.div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-9 w-9" />
|
||||
<Skeleton className="h-9 w-9" />
|
||||
<Skeleton className="h-9 w-9" />
|
||||
<Skeleton className="h-9 w-9" />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-4 p-4">
|
||||
<Skeleton className="h-4 w-64" />
|
||||
<Skeleton className="h-32 w-full max-w-2xl rounded-xl" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -30,8 +30,10 @@ import {
|
|||
// extractWriteTodosFromContent,
|
||||
} from "@/atoms/chat/plan-state.atom";
|
||||
import { closeReportPanelAtom } from "@/atoms/chat/report-panel.atom";
|
||||
import { type AgentCreatedDocument, agentCreatedDocumentsAtom } from "@/atoms/documents/ui.atoms";
|
||||
import { closeEditorPanelAtom } from "@/atoms/editor/editor-panel.atom";
|
||||
import { membersAtom } from "@/atoms/members/members-query.atoms";
|
||||
import { updateChatTabTitleAtom } from "@/atoms/tabs/tabs.atom";
|
||||
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
||||
import { ThinkingStepsDataUI } from "@/components/assistant-ui/thinking-steps";
|
||||
import { Thread } from "@/components/assistant-ui/thread";
|
||||
|
|
@ -74,6 +76,7 @@ import {
|
|||
trackChatMessageSent,
|
||||
trackChatResponseReceived,
|
||||
} from "@/lib/posthog/events";
|
||||
import Loading from "../loading";
|
||||
|
||||
/**
|
||||
* After a tool produces output, mark any previously-decided interrupt tool
|
||||
|
|
@ -188,6 +191,8 @@ export default function NewChatPage() {
|
|||
const clearTargetCommentId = useSetAtom(clearTargetCommentIdAtom);
|
||||
const closeReportPanel = useSetAtom(closeReportPanelAtom);
|
||||
const closeEditorPanel = useSetAtom(closeEditorPanelAtom);
|
||||
const updateChatTabTitle = useSetAtom(updateChatTabTitleAtom);
|
||||
const setAgentCreatedDocuments = useSetAtom(agentCreatedDocumentsAtom);
|
||||
|
||||
// Get current user for author info in shared chats
|
||||
const { data: currentUser } = useAtomValue(currentUserAtom);
|
||||
|
|
@ -726,12 +731,10 @@ export default function NewChatPage() {
|
|||
}
|
||||
|
||||
case "data-thread-title-update": {
|
||||
// Handle thread title update from LLM-generated title
|
||||
const titleData = parsed.data as { threadId: number; title: string };
|
||||
if (titleData?.title && titleData?.threadId === currentThreadId) {
|
||||
// Update current thread state with new title
|
||||
setCurrentThread((prev) => (prev ? { ...prev, title: titleData.title } : prev));
|
||||
// Invalidate thread list to refresh sidebar
|
||||
updateChatTabTitle({ chatId: currentThreadId, title: titleData.title });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["threads", String(searchSpaceId)],
|
||||
});
|
||||
|
|
@ -739,6 +742,20 @@ export default function NewChatPage() {
|
|||
break;
|
||||
}
|
||||
|
||||
case "data-documents-updated": {
|
||||
const docEvent = parsed.data as {
|
||||
action: string;
|
||||
document: AgentCreatedDocument;
|
||||
};
|
||||
if (docEvent?.document?.id) {
|
||||
setAgentCreatedDocuments((prev) => {
|
||||
if (prev.some((d) => d.id === docEvent.document.id)) return prev;
|
||||
return [...prev, docEvent.document];
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "data-interrupt-request": {
|
||||
wasInterrupted = true;
|
||||
const interruptData = parsed.data as Record<string, unknown>;
|
||||
|
|
@ -1526,49 +1543,14 @@ export default function NewChatPage() {
|
|||
|
||||
// Show loading state only when loading an existing thread
|
||||
if (isInitializing) {
|
||||
return (
|
||||
<div className="flex h-[calc(100dvh-64px)] flex-col bg-main-panel px-4">
|
||||
<div className="mx-auto w-full max-w-[44rem] flex flex-1 flex-col gap-6 py-8">
|
||||
{/* User message */}
|
||||
<div className="flex justify-end">
|
||||
<Skeleton className="h-12 w-56 rounded-2xl" />
|
||||
</div>
|
||||
|
||||
{/* Assistant message */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-[85%]" />
|
||||
<Skeleton className="h-4 w-[70%]" />
|
||||
</div>
|
||||
|
||||
{/* User message */}
|
||||
<div className="flex justify-end">
|
||||
<Skeleton className="h-12 w-40 rounded-2xl" />
|
||||
</div>
|
||||
|
||||
{/* Assistant message */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-[90%]" />
|
||||
<Skeleton className="h-4 w-[60%]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Input bar */}
|
||||
<div className="sticky bottom-0 pb-6 bg-main-panel">
|
||||
<div className="mx-auto w-full max-w-[44rem]">
|
||||
<Skeleton className="h-24 w-full rounded-2xl" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
// Show error state only if we tried to load an existing thread but failed
|
||||
// For new chats (urlChatId === 0), threadId being null is expected (lazy creation)
|
||||
if (!threadId && urlChatId > 0) {
|
||||
return (
|
||||
<div className="flex h-[calc(100dvh-64px)] flex-col items-center justify-center gap-4">
|
||||
<div className="flex h-full flex-col items-center justify-center gap-4">
|
||||
<div className="text-destructive">Failed to load chat</div>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -1587,7 +1569,7 @@ export default function NewChatPage() {
|
|||
return (
|
||||
<AssistantRuntimeProvider runtime={runtime}>
|
||||
<ThinkingStepsDataUI />
|
||||
<div key={searchSpaceId} className="flex h-[calc(100dvh-64px)] overflow-hidden">
|
||||
<div key={searchSpaceId} className="flex h-full overflow-hidden">
|
||||
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
||||
<Thread />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-main-panel px-4">
|
||||
<div className="mx-auto w-full max-w-[44rem] flex flex-1 flex-col gap-6 py-8">
|
||||
{/* User message */}
|
||||
<div className="flex justify-end">
|
||||
<Skeleton className="h-12 w-56 rounded-2xl" />
|
||||
</div>
|
||||
|
||||
{/* Assistant message */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-[85%]" />
|
||||
<Skeleton className="h-18 w-[40%]" />
|
||||
</div>
|
||||
|
||||
{/* User message */}
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Skeleton className="h-12 w-72 rounded-2xl" />
|
||||
</div>
|
||||
|
||||
{/* Assistant message */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-10 w-[30%]" />
|
||||
<Skeleton className="h-4 w-[90%]" />
|
||||
<Skeleton className="h-6 w-[60%]" />
|
||||
</div>
|
||||
|
||||
{/* User message */}
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Skeleton className="h-12 w-96 rounded-2xl" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Input bar */}
|
||||
<div className="sticky bottom-0 pb-6 bg-main-panel">
|
||||
<div className="mx-auto w-full max-w-[44rem]">
|
||||
<Skeleton className="h-24 w-full rounded-2xl" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,15 +1,10 @@
|
|||
"use client";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function SearchSpaceDashboardPage() {
|
||||
const router = useRouter();
|
||||
const { search_space_id } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
router.push(`/dashboard/${search_space_id}/new-chat`);
|
||||
}, [router, search_space_id]);
|
||||
|
||||
return <></>;
|
||||
export default async function SearchSpaceDashboardPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ search_space_id: string }>;
|
||||
}) {
|
||||
const { search_space_id } = await params;
|
||||
redirect(`/dashboard/${search_space_id}/new-chat`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -188,13 +188,13 @@ export function TeamContent({ searchSpaceId }: TeamContentProps) {
|
|||
[deleteMember, searchSpaceId]
|
||||
);
|
||||
|
||||
const { data: roles = [] } = useQuery({
|
||||
const { data: roles = [], isLoading: rolesLoading } = useQuery({
|
||||
queryKey: cacheKeys.roles.all(searchSpaceId.toString()),
|
||||
queryFn: () => rolesApiService.getRoles({ search_space_id: searchSpaceId }),
|
||||
enabled: !!searchSpaceId,
|
||||
});
|
||||
|
||||
const { data: invites = [] } = useQuery({
|
||||
const { data: invites = [], isLoading: invitesLoading } = useQuery({
|
||||
queryKey: cacheKeys.invites.all(searchSpaceId.toString()),
|
||||
queryFn: () => invitesApiService.getInvites({ search_space_id: searchSpaceId }),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
|
|
@ -294,15 +294,23 @@ export function TeamContent({ searchSpaceId }: TeamContentProps) {
|
|||
return (
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{canInvite && (
|
||||
<CreateInviteDialog
|
||||
roles={roles}
|
||||
onCreateInvite={handleCreateInvite}
|
||||
searchSpaceId={searchSpaceId}
|
||||
/>
|
||||
{rolesLoading ? (
|
||||
<Skeleton className="h-9 w-32 rounded-md" />
|
||||
) : (
|
||||
canInvite && (
|
||||
<CreateInviteDialog
|
||||
roles={roles}
|
||||
onCreateInvite={handleCreateInvite}
|
||||
searchSpaceId={searchSpaceId}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{canInvite && activeInvites.length > 0 && (
|
||||
<AllInvitesDialog invites={activeInvites} onRevokeInvite={handleRevokeInvite} />
|
||||
{invitesLoading ? (
|
||||
<Skeleton className="h-9 w-32 rounded-md" />
|
||||
) : (
|
||||
canInvite && activeInvites.length > 0 && (
|
||||
<AllInvitesDialog invites={activeInvites} onRevokeInvite={handleRevokeInvite} />
|
||||
)
|
||||
)}
|
||||
<p className="text-xs md:text-sm text-muted-foreground whitespace-nowrap">
|
||||
{members.length} {members.length === 1 ? "member" : "members"}
|
||||
|
|
@ -595,6 +603,7 @@ function CreateInviteDialog({
|
|||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to create invite:", error);
|
||||
toast.error("Failed to create invite. Please try again.");
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
import posthog from "posthog-js";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function Error({
|
||||
export default function ErrorPage({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
error: globalThis.Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import NextError from "next/error";
|
||||
import "./globals.css";
|
||||
import posthog from "posthog-js";
|
||||
import { useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export default function GlobalError({
|
||||
error,
|
||||
|
|
@ -18,10 +19,11 @@ export default function GlobalError({
|
|||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<NextError statusCode={0} />
|
||||
<button type="button" onClick={reset}>
|
||||
Try again
|
||||
</button>
|
||||
<div className="flex min-h-screen flex-col items-center justify-center gap-4 p-4">
|
||||
<h2 className="text-xl font-semibold">Something went wrong</h2>
|
||||
<p className="text-sm text-muted-foreground">An unexpected error occurred.</p>
|
||||
<Button onClick={reset}>Try again</Button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import { PublicChatView } from "@/components/public-chat/public-chat-view";
|
||||
|
||||
export default function PublicChatPage() {
|
||||
const params = useParams();
|
||||
const token = params.token as string;
|
||||
export default async function PublicChatPage({ params }: { params: Promise<{ token: string }> }) {
|
||||
const { token } = await params;
|
||||
|
||||
return <PublicChatView shareToken={token} />;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue