Spaces:
Running
Running
Merge pull request #305 from saurabhhhcodes/frontend/skeleton-loading-states-275
Browse files
frontend/src/app/dashboard/page.tsx
CHANGED
|
@@ -66,6 +66,7 @@ export default function DashboardPage() {
|
|
| 66 |
const [sidebarOpen, setSidebarOpen] = useState(true);
|
| 67 |
const [viewerOpen, setViewerOpen] = useState(true);
|
| 68 |
const [connectionError, setConnectionError] = useState("");
|
|
|
|
| 69 |
|
| 70 |
// Auth guard
|
| 71 |
useEffect(() => {
|
|
@@ -86,6 +87,7 @@ export default function DashboardPage() {
|
|
| 86 |
|
| 87 |
// Load documents
|
| 88 |
const loadDocuments = useCallback(async () => {
|
|
|
|
| 89 |
try {
|
| 90 |
const data = await api.get<{ documents?: DocInfo[]; items?: DocInfo[] }>(
|
| 91 |
"/api/v1/documents/"
|
|
@@ -99,6 +101,8 @@ export default function DashboardPage() {
|
|
| 99 |
? CONNECTION_ERROR_BANNER_MESSAGE
|
| 100 |
: `⚠️ ${message}`
|
| 101 |
);
|
|
|
|
|
|
|
| 102 |
}
|
| 103 |
}, []);
|
| 104 |
|
|
@@ -152,6 +156,7 @@ export default function DashboardPage() {
|
|
| 152 |
<DocumentSidebar
|
| 153 |
documents={documents}
|
| 154 |
activeDoc={activeDoc}
|
|
|
|
| 155 |
onSelectDoc={(doc) => {
|
| 156 |
setActiveDoc(doc);
|
| 157 |
setPdfPage(1);
|
|
|
|
| 66 |
const [sidebarOpen, setSidebarOpen] = useState(true);
|
| 67 |
const [viewerOpen, setViewerOpen] = useState(true);
|
| 68 |
const [connectionError, setConnectionError] = useState("");
|
| 69 |
+
const [documentsLoading, setDocumentsLoading] = useState(true);
|
| 70 |
|
| 71 |
// Auth guard
|
| 72 |
useEffect(() => {
|
|
|
|
| 87 |
|
| 88 |
// Load documents
|
| 89 |
const loadDocuments = useCallback(async () => {
|
| 90 |
+
setDocumentsLoading(true);
|
| 91 |
try {
|
| 92 |
const data = await api.get<{ documents?: DocInfo[]; items?: DocInfo[] }>(
|
| 93 |
"/api/v1/documents/"
|
|
|
|
| 101 |
? CONNECTION_ERROR_BANNER_MESSAGE
|
| 102 |
: `⚠️ ${message}`
|
| 103 |
);
|
| 104 |
+
} finally {
|
| 105 |
+
setDocumentsLoading(false);
|
| 106 |
}
|
| 107 |
}, []);
|
| 108 |
|
|
|
|
| 156 |
<DocumentSidebar
|
| 157 |
documents={documents}
|
| 158 |
activeDoc={activeDoc}
|
| 159 |
+
loading={documentsLoading}
|
| 160 |
onSelectDoc={(doc) => {
|
| 161 |
setActiveDoc(doc);
|
| 162 |
setPdfPage(1);
|
frontend/src/components/chat/ChatPanel.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import type { DocInfo } from "@/app/dashboard/page";
|
|
| 6 |
import { api, API_BASE } from "@/lib/api";
|
| 7 |
import { useChatStore, type ChatMsg, type SourceChunk } from "@/store/chat-store";
|
| 8 |
import { Button } from "@/components/ui/button";
|
|
|
|
| 9 |
import { Textarea } from "@/components/ui/textarea";
|
| 10 |
import MessageBubble from "./MessageBubble";
|
| 11 |
import SourceCard from "./SourceCard";
|
|
@@ -57,6 +58,7 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) {
|
|
| 57 |
const input = useChatStore((state) => state.input);
|
| 58 |
const streaming = useChatStore((state) => state.streaming);
|
| 59 |
const isTyping = useChatStore((state) => state.isTyping);
|
|
|
|
| 60 |
const activeSessionId = useChatStore((state) => state.activeSessionId);
|
| 61 |
const setMessages = useChatStore((state) => state.setMessages);
|
| 62 |
const setInput = useChatStore((state) => state.setInput);
|
|
@@ -74,6 +76,8 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) {
|
|
| 74 |
const prevDocId = useRef<string | null>(null);
|
| 75 |
const exportMenuRef = useRef<HTMLDivElement>(null);
|
| 76 |
|
|
|
|
|
|
|
| 77 |
useEffect(() => {
|
| 78 |
const textarea = textareaRef.current;
|
| 79 |
if (!textarea) return;
|
|
@@ -397,8 +401,24 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) {
|
|
| 397 |
return (
|
| 398 |
<div className="h-full flex flex-col">
|
| 399 |
{/* ── Chat Messages ──────────────────────────── */}
|
| 400 |
-
|
| 401 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
<div className="h-full flex flex-col items-center justify-center py-20">
|
| 403 |
<div className="w-16 h-16 rounded-2xl bg-primary/10 flex items-center justify-center mb-4">
|
| 404 |
<MessageSquare className="w-8 h-8 text-primary/60" />
|
|
|
|
| 6 |
import { api, API_BASE } from "@/lib/api";
|
| 7 |
import { useChatStore, type ChatMsg, type SourceChunk } from "@/store/chat-store";
|
| 8 |
import { Button } from "@/components/ui/button";
|
| 9 |
+
import { Skeleton } from "@/components/ui/skeleton";
|
| 10 |
import { Textarea } from "@/components/ui/textarea";
|
| 11 |
import MessageBubble from "./MessageBubble";
|
| 12 |
import SourceCard from "./SourceCard";
|
|
|
|
| 58 |
const input = useChatStore((state) => state.input);
|
| 59 |
const streaming = useChatStore((state) => state.streaming);
|
| 60 |
const isTyping = useChatStore((state) => state.isTyping);
|
| 61 |
+
const historyLoading = useChatStore((state) => state.historyLoading);
|
| 62 |
const activeSessionId = useChatStore((state) => state.activeSessionId);
|
| 63 |
const setMessages = useChatStore((state) => state.setMessages);
|
| 64 |
const setInput = useChatStore((state) => state.setInput);
|
|
|
|
| 76 |
const prevDocId = useRef<string | null>(null);
|
| 77 |
const exportMenuRef = useRef<HTMLDivElement>(null);
|
| 78 |
|
| 79 |
+
const showEmptyState = messages.length === 0 && !isTyping && !historyLoading;
|
| 80 |
+
|
| 81 |
useEffect(() => {
|
| 82 |
const textarea = textareaRef.current;
|
| 83 |
if (!textarea) return;
|
|
|
|
| 401 |
return (
|
| 402 |
<div className="h-full flex flex-col">
|
| 403 |
{/* ── Chat Messages ──────────────────────────── */}
|
| 404 |
+
<div className="flex-1 px-4 overflow-y-auto custom-scrollbar" aria-busy={historyLoading}>
|
| 405 |
+
{historyLoading ? (
|
| 406 |
+
<div className="py-6 space-y-5 max-w-3xl mx-auto" aria-label="Loading chat history">
|
| 407 |
+
{Array.from({ length: 4 }).map((_, index) => (
|
| 408 |
+
<div
|
| 409 |
+
key={index}
|
| 410 |
+
className={cn("flex gap-3", index % 2 === 0 ? "justify-end" : "justify-start")}
|
| 411 |
+
>
|
| 412 |
+
{index % 2 !== 0 && <Skeleton className="mt-1 h-8 w-8 rounded-full" />}
|
| 413 |
+
<div className={cn("space-y-2", index % 2 === 0 ? "w-2/3" : "w-3/4")}>
|
| 414 |
+
<Skeleton className="h-4 w-full" />
|
| 415 |
+
<Skeleton className="h-4 w-5/6" />
|
| 416 |
+
<Skeleton className="h-4 w-2/3" />
|
| 417 |
+
</div>
|
| 418 |
+
</div>
|
| 419 |
+
))}
|
| 420 |
+
</div>
|
| 421 |
+
) : showEmptyState ? (
|
| 422 |
<div className="h-full flex flex-col items-center justify-center py-20">
|
| 423 |
<div className="w-16 h-16 rounded-2xl bg-primary/10 flex items-center justify-center mb-4">
|
| 424 |
<MessageSquare className="w-8 h-8 text-primary/60" />
|
frontend/src/components/document/DocumentSidebar.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
|
|
| 5 |
import type { DocInfo } from "@/app/dashboard/page";
|
| 6 |
import { api } from "@/lib/api";
|
| 7 |
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
|
|
| 8 |
import { Button } from "@/components/ui/button";
|
| 9 |
import { Badge } from "@/components/ui/badge";
|
| 10 |
import { Progress } from "@/components/ui/progress";
|
|
@@ -19,11 +20,34 @@ import { toast } from "sonner";
|
|
| 19 |
interface Props {
|
| 20 |
documents: DocInfo[];
|
| 21 |
activeDoc: DocInfo | null;
|
|
|
|
| 22 |
onSelectDoc: (doc: DocInfo) => void;
|
| 23 |
onDocumentsChange: () => void;
|
| 24 |
}
|
| 25 |
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
const { t } = useTranslation();
|
| 28 |
const [uploading, setUploading] = useState(false);
|
| 29 |
const [uploadProgress, setUploadProgress] = useState(0);
|
|
@@ -155,12 +179,16 @@ export default function DocumentSidebar({ documents = [], activeDoc, onSelectDoc
|
|
| 155 |
{/* ── Documents List ──────────────────────────── */}
|
| 156 |
<div className="px-3 pt-3 pb-1">
|
| 157 |
<h3 className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-2">
|
| 158 |
-
{
|
|
|
|
|
|
|
| 159 |
</h3>
|
| 160 |
</div>
|
| 161 |
|
| 162 |
-
<ScrollArea className="flex-1 px-3 overflow-auto">
|
| 163 |
-
{
|
|
|
|
|
|
|
| 164 |
<div className="text-center py-12">
|
| 165 |
<FolderOpen className="w-8 h-8 mx-auto text-muted-foreground/40 mb-3" />
|
| 166 |
<p className="text-sm text-muted-foreground">{t("documents.noDocuments")}</p>
|
|
|
|
| 5 |
import type { DocInfo } from "@/app/dashboard/page";
|
| 6 |
import { api } from "@/lib/api";
|
| 7 |
import { ScrollArea } from "@/components/ui/scroll-area";
|
| 8 |
+
import { Skeleton } from "@/components/ui/skeleton";
|
| 9 |
import { Button } from "@/components/ui/button";
|
| 10 |
import { Badge } from "@/components/ui/badge";
|
| 11 |
import { Progress } from "@/components/ui/progress";
|
|
|
|
| 20 |
interface Props {
|
| 21 |
documents: DocInfo[];
|
| 22 |
activeDoc: DocInfo | null;
|
| 23 |
+
loading?: boolean;
|
| 24 |
onSelectDoc: (doc: DocInfo) => void;
|
| 25 |
onDocumentsChange: () => void;
|
| 26 |
}
|
| 27 |
|
| 28 |
+
function DocumentListSkeleton() {
|
| 29 |
+
return (
|
| 30 |
+
<div className="space-y-2 pb-3" aria-hidden="true">
|
| 31 |
+
{Array.from({ length: 5 }).map((_, index) => (
|
| 32 |
+
<div key={index} className="rounded-lg border border-sidebar-border/60 p-2.5">
|
| 33 |
+
<div className="flex items-start gap-2.5">
|
| 34 |
+
<Skeleton className="mt-0.5 h-4 w-4 rounded-full bg-sidebar-accent" />
|
| 35 |
+
<div className="min-w-0 flex-1 space-y-2">
|
| 36 |
+
<Skeleton className="h-4 w-4/5 bg-sidebar-accent" />
|
| 37 |
+
<Skeleton className="h-3 w-full bg-sidebar-accent/80" />
|
| 38 |
+
<div className="flex gap-2">
|
| 39 |
+
<Skeleton className="h-3 w-10 bg-sidebar-accent/70" />
|
| 40 |
+
<Skeleton className="h-3 w-12 bg-sidebar-accent/70" />
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
</div>
|
| 45 |
+
))}
|
| 46 |
+
</div>
|
| 47 |
+
);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
export default function DocumentSidebar({ documents = [], activeDoc, loading = false, onSelectDoc, onDocumentsChange }: Props) {
|
| 51 |
const { t } = useTranslation();
|
| 52 |
const [uploading, setUploading] = useState(false);
|
| 53 |
const [uploadProgress, setUploadProgress] = useState(0);
|
|
|
|
| 179 |
{/* ── Documents List ──────────────────────────── */}
|
| 180 |
<div className="px-3 pt-3 pb-1">
|
| 181 |
<h3 className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-2">
|
| 182 |
+
{loading
|
| 183 |
+
? t("documents.documentsTitle", { count: "..." })
|
| 184 |
+
: t("documents.documentsTitle", { count: documents.length })}
|
| 185 |
</h3>
|
| 186 |
</div>
|
| 187 |
|
| 188 |
+
<ScrollArea className="flex-1 px-3 overflow-auto" aria-busy={loading}>
|
| 189 |
+
{loading ? (
|
| 190 |
+
<DocumentListSkeleton />
|
| 191 |
+
) : documents.length === 0 ? (
|
| 192 |
<div className="text-center py-12">
|
| 193 |
<FolderOpen className="w-8 h-8 mx-auto text-muted-foreground/40 mb-3" />
|
| 194 |
<p className="text-sm text-muted-foreground">{t("documents.noDocuments")}</p>
|
frontend/src/components/layout/Header.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { useState } from "react";
|
|
| 4 |
import { useAuth } from "@/lib/auth";
|
| 5 |
import { useRouter } from "next/navigation";
|
| 6 |
import { Button } from "@/components/ui/button";
|
|
|
|
| 7 |
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
| 8 |
import {
|
| 9 |
DropdownMenu,
|
|
@@ -56,6 +57,7 @@ export default function Header({
|
|
| 56 |
const { theme, setTheme } = useTheme();
|
| 57 |
const mounted = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
| 58 |
const [sheetOpen, setSheetOpen] = useState(false);
|
|
|
|
| 59 |
const workspace = useWorkspaceStore((s) => s.workspace);
|
| 60 |
const setWorkspace = useWorkspaceStore((s) => s.setWorkspace);
|
| 61 |
|
|
@@ -69,6 +71,7 @@ export default function Header({
|
|
| 69 |
|
| 70 |
const fetchDocumentsForWorkspace = async (id: string) => {
|
| 71 |
// Placeholder: simulate fetching documents for the selected workspace
|
|
|
|
| 72 |
try {
|
| 73 |
// Attempt a real API call if available; otherwise this will fail silently
|
| 74 |
const res = await api.get(`/api/v1/documents?workspace=${encodeURIComponent(id)}`).catch(() => null);
|
|
@@ -77,6 +80,8 @@ export default function Header({
|
|
| 77 |
// e.g. documentStore.setDocuments(res || [])
|
| 78 |
} catch (err) {
|
| 79 |
console.warn("Failed to fetch documents for workspace", id, err);
|
|
|
|
|
|
|
| 80 |
}
|
| 81 |
};
|
| 82 |
|
|
@@ -152,9 +157,18 @@ export default function Header({
|
|
| 152 |
{/* Workspace switcher */}
|
| 153 |
<DropdownMenu>
|
| 154 |
<DropdownMenuTrigger className="flex items-center h-8 gap-2 px-2 rounded-md hover:bg-accent transition-colors cursor-pointer">
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
</DropdownMenuTrigger>
|
| 159 |
<DropdownMenuContent align="end" className="w-40">
|
| 160 |
{WORKSPACES.map((w) => (
|
|
@@ -247,4 +261,4 @@ export default function Header({
|
|
| 247 |
</aside>
|
| 248 |
</>
|
| 249 |
);
|
| 250 |
-
}
|
|
|
|
| 4 |
import { useAuth } from "@/lib/auth";
|
| 5 |
import { useRouter } from "next/navigation";
|
| 6 |
import { Button } from "@/components/ui/button";
|
| 7 |
+
import { Skeleton } from "@/components/ui/skeleton";
|
| 8 |
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
| 9 |
import {
|
| 10 |
DropdownMenu,
|
|
|
|
| 57 |
const { theme, setTheme } = useTheme();
|
| 58 |
const mounted = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
| 59 |
const [sheetOpen, setSheetOpen] = useState(false);
|
| 60 |
+
const [workspaceLoading, setWorkspaceLoading] = useState(false);
|
| 61 |
const workspace = useWorkspaceStore((s) => s.workspace);
|
| 62 |
const setWorkspace = useWorkspaceStore((s) => s.setWorkspace);
|
| 63 |
|
|
|
|
| 71 |
|
| 72 |
const fetchDocumentsForWorkspace = async (id: string) => {
|
| 73 |
// Placeholder: simulate fetching documents for the selected workspace
|
| 74 |
+
setWorkspaceLoading(true);
|
| 75 |
try {
|
| 76 |
// Attempt a real API call if available; otherwise this will fail silently
|
| 77 |
const res = await api.get(`/api/v1/documents?workspace=${encodeURIComponent(id)}`).catch(() => null);
|
|
|
|
| 80 |
// e.g. documentStore.setDocuments(res || [])
|
| 81 |
} catch (err) {
|
| 82 |
console.warn("Failed to fetch documents for workspace", id, err);
|
| 83 |
+
} finally {
|
| 84 |
+
setWorkspaceLoading(false);
|
| 85 |
}
|
| 86 |
};
|
| 87 |
|
|
|
|
| 157 |
{/* Workspace switcher */}
|
| 158 |
<DropdownMenu>
|
| 159 |
<DropdownMenuTrigger className="flex items-center h-8 gap-2 px-2 rounded-md hover:bg-accent transition-colors cursor-pointer">
|
| 160 |
+
{workspaceLoading ? (
|
| 161 |
+
<>
|
| 162 |
+
<Skeleton className="h-4 w-4 rounded-sm" />
|
| 163 |
+
<Skeleton className="hidden h-4 w-16 sm:block" />
|
| 164 |
+
</>
|
| 165 |
+
) : (
|
| 166 |
+
<>
|
| 167 |
+
<Briefcase className="w-4 h-4" />
|
| 168 |
+
<span className="text-sm hidden sm:inline">{WORKSPACES.find((w) => w.id === workspace)?.label}</span>
|
| 169 |
+
<ChevronDown className="w-3 h-3" />
|
| 170 |
+
</>
|
| 171 |
+
)}
|
| 172 |
</DropdownMenuTrigger>
|
| 173 |
<DropdownMenuContent align="end" className="w-40">
|
| 174 |
{WORKSPACES.map((w) => (
|
|
|
|
| 261 |
</aside>
|
| 262 |
</>
|
| 263 |
);
|
| 264 |
+
}
|
frontend/src/store/chat-store.ts
CHANGED
|
@@ -32,12 +32,14 @@ interface ChatStore {
|
|
| 32 |
input: string;
|
| 33 |
streaming: boolean;
|
| 34 |
isTyping: boolean;
|
|
|
|
| 35 |
sessions: ChatSession[];
|
| 36 |
activeSessionId: string | null;
|
| 37 |
setMessages: (value: Setter<ChatMsg[]>) => void;
|
| 38 |
setInput: (value: Setter<string>) => void;
|
| 39 |
setStreaming: (value: Setter<boolean>) => void;
|
| 40 |
setIsTyping: (value: Setter<boolean>) => void;
|
|
|
|
| 41 |
setSessions: (value: Setter<ChatSession[]>) => void;
|
| 42 |
setActiveSessionId: (value: Setter<string | null>) => void;
|
| 43 |
fetchSessions: () => Promise<void>;
|
|
@@ -56,6 +58,7 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
|
| 56 |
input: "",
|
| 57 |
streaming: false,
|
| 58 |
isTyping: false,
|
|
|
|
| 59 |
sessions: [],
|
| 60 |
activeSessionId: null,
|
| 61 |
|
|
@@ -75,6 +78,10 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
|
| 75 |
set((state) => ({ isTyping: resolveValue(value, state.isTyping) }));
|
| 76 |
},
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
setSessions(value) {
|
| 79 |
set((state) => ({ sessions: resolveValue(value, state.sessions) }));
|
| 80 |
},
|
|
@@ -150,11 +157,14 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
|
| 150 |
},
|
| 151 |
|
| 152 |
async fetchSessionHistory(id) {
|
|
|
|
| 153 |
try {
|
| 154 |
const data = await api.get<{ messages: ChatMsg[] }>(`/api/v1/chat/history/session/${id}`);
|
| 155 |
set({ messages: data.messages });
|
| 156 |
} catch (err) {
|
| 157 |
console.error("Failed to fetch session history:", err);
|
|
|
|
|
|
|
| 158 |
}
|
| 159 |
},
|
| 160 |
|
|
@@ -164,6 +174,7 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
|
| 164 |
input: "",
|
| 165 |
streaming: false,
|
| 166 |
isTyping: false,
|
|
|
|
| 167 |
sessions: [],
|
| 168 |
activeSessionId: null,
|
| 169 |
});
|
|
|
|
| 32 |
input: string;
|
| 33 |
streaming: boolean;
|
| 34 |
isTyping: boolean;
|
| 35 |
+
historyLoading: boolean;
|
| 36 |
sessions: ChatSession[];
|
| 37 |
activeSessionId: string | null;
|
| 38 |
setMessages: (value: Setter<ChatMsg[]>) => void;
|
| 39 |
setInput: (value: Setter<string>) => void;
|
| 40 |
setStreaming: (value: Setter<boolean>) => void;
|
| 41 |
setIsTyping: (value: Setter<boolean>) => void;
|
| 42 |
+
setHistoryLoading: (value: Setter<boolean>) => void;
|
| 43 |
setSessions: (value: Setter<ChatSession[]>) => void;
|
| 44 |
setActiveSessionId: (value: Setter<string | null>) => void;
|
| 45 |
fetchSessions: () => Promise<void>;
|
|
|
|
| 58 |
input: "",
|
| 59 |
streaming: false,
|
| 60 |
isTyping: false,
|
| 61 |
+
historyLoading: false,
|
| 62 |
sessions: [],
|
| 63 |
activeSessionId: null,
|
| 64 |
|
|
|
|
| 78 |
set((state) => ({ isTyping: resolveValue(value, state.isTyping) }));
|
| 79 |
},
|
| 80 |
|
| 81 |
+
setHistoryLoading(value) {
|
| 82 |
+
set((state) => ({ historyLoading: resolveValue(value, state.historyLoading) }));
|
| 83 |
+
},
|
| 84 |
+
|
| 85 |
setSessions(value) {
|
| 86 |
set((state) => ({ sessions: resolveValue(value, state.sessions) }));
|
| 87 |
},
|
|
|
|
| 157 |
},
|
| 158 |
|
| 159 |
async fetchSessionHistory(id) {
|
| 160 |
+
set({ historyLoading: true });
|
| 161 |
try {
|
| 162 |
const data = await api.get<{ messages: ChatMsg[] }>(`/api/v1/chat/history/session/${id}`);
|
| 163 |
set({ messages: data.messages });
|
| 164 |
} catch (err) {
|
| 165 |
console.error("Failed to fetch session history:", err);
|
| 166 |
+
} finally {
|
| 167 |
+
set({ historyLoading: false });
|
| 168 |
}
|
| 169 |
},
|
| 170 |
|
|
|
|
| 174 |
input: "",
|
| 175 |
streaming: false,
|
| 176 |
isTyping: false,
|
| 177 |
+
historyLoading: false,
|
| 178 |
sessions: [],
|
| 179 |
activeSessionId: null,
|
| 180 |
});
|