feat(frontend): add notebook selection and management UI
Implement complete frontend integration for NotebookLM + RAG: New Components: - Notebooks.tsx: Full notebook management page - View indexed notebooks with stats - Sync available notebooks from NotebookLM - Delete notebook indexes - Tab-based interface (Indexed/Available) Updated Components: - Chat.tsx: Enhanced with notebook selector - Collapsible notebook selection panel - Multi-notebook selection with checkboxes - 'Include documents' toggle - Visual indicators for notebook sources in chat - Shows selected notebook count in header New Store: - notebookStore.ts: Zustand store for notebook state - Manage selected notebooks - Track sync status - Fetch indexed/available notebooks - Handle sync/delete operations API Client Updates: - Added notebook-related endpoints: - getNotebooks(), getIndexedNotebooks() - syncNotebook(), deleteNotebookIndex() - getSyncStatus(), queryWithNotebooks() Type Definitions: - Notebook, IndexedNotebook, SyncStatus - SyncResponse, NotebookLMSource - Extended Source types for notebook metadata UI/UX: - Added Notebooks to navigation menu - Stats cards showing indexed/total sources/chunks - Color-coded source badges (blue for notebooks) - Warning when no notebooks indexed - Loading states for all operations Closes frontend integration requirement
This commit is contained in:
@@ -4,6 +4,7 @@ import { Layout } from '@/components/layout/Layout';
|
|||||||
import { Login } from '@/pages/Login';
|
import { Login } from '@/pages/Login';
|
||||||
import { Dashboard } from '@/pages/Dashboard';
|
import { Dashboard } from '@/pages/Dashboard';
|
||||||
import { Documents } from '@/pages/Documents';
|
import { Documents } from '@/pages/Documents';
|
||||||
|
import { Notebooks } from '@/pages/Notebooks';
|
||||||
import { Chat } from '@/pages/Chat';
|
import { Chat } from '@/pages/Chat';
|
||||||
import { Settings } from '@/pages/Settings';
|
import { Settings } from '@/pages/Settings';
|
||||||
import { useAuthStore } from '@/stores/authStore';
|
import { useAuthStore } from '@/stores/authStore';
|
||||||
@@ -81,6 +82,14 @@ function AppContent() {
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/notebooks"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<Notebooks />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/chat"
|
path="/chat"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -112,6 +112,69 @@ class ApiClient {
|
|||||||
return this.put('/config/provider', { provider, model });
|
return this.put('/config/provider', { provider, model });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NotebookLM Integration API
|
||||||
|
async getNotebooks(): Promise<Notebook[]> {
|
||||||
|
return this.get<Notebook[]>('/notebooks');
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncNotebook(notebookId: string): Promise<SyncResponse> {
|
||||||
|
return this.post<SyncResponse>(`/notebooklm/sync/${notebookId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getIndexedNotebooks(): Promise<{ notebooks: IndexedNotebook[]; total: number }> {
|
||||||
|
return this.get<{ notebooks: IndexedNotebook[]; total: number }>('/notebooklm/indexed');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSyncStatus(notebookId: string): Promise<SyncStatus> {
|
||||||
|
return this.get<SyncStatus>(`/notebooklm/sync/${notebookId}/status`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteNotebookIndex(notebookId: string): Promise<{ notebook_id: string; deleted: boolean; message: string }> {
|
||||||
|
return this.delete<{ notebook_id: string; deleted: boolean; message: string }>(`/notebooklm/sync/${notebookId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async queryWithNotebooks(
|
||||||
|
question: string,
|
||||||
|
notebookIds: string[],
|
||||||
|
options?: {
|
||||||
|
includeDocuments?: boolean;
|
||||||
|
provider?: string;
|
||||||
|
model?: string;
|
||||||
|
k?: number;
|
||||||
|
}
|
||||||
|
): Promise<{
|
||||||
|
question: string;
|
||||||
|
answer: string;
|
||||||
|
sources: (Source | NotebookLMSource)[];
|
||||||
|
provider: string;
|
||||||
|
model: string;
|
||||||
|
filters_applied?: {
|
||||||
|
notebook_ids: string[];
|
||||||
|
include_documents: boolean;
|
||||||
|
};
|
||||||
|
}> {
|
||||||
|
if (options?.includeDocuments !== false) {
|
||||||
|
// Query mista (documenti + notebook)
|
||||||
|
return this.post('/query', {
|
||||||
|
question,
|
||||||
|
notebook_ids: notebookIds,
|
||||||
|
include_documents: options?.includeDocuments ?? true,
|
||||||
|
provider: options?.provider,
|
||||||
|
model: options?.model,
|
||||||
|
k: options?.k ?? 5,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Query solo sui notebook
|
||||||
|
return this.post('/query/notebooks', {
|
||||||
|
question,
|
||||||
|
notebook_ids: notebookIds,
|
||||||
|
provider: options?.provider,
|
||||||
|
model: options?.model,
|
||||||
|
k: options?.k ?? 5,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
async healthCheck(): Promise<{ status: string; version: string }> {
|
async healthCheck(): Promise<{ status: string; version: string }> {
|
||||||
const response = await axios.get(`${API_BASE_URL}/api/health`);
|
const response = await axios.get(`${API_BASE_URL}/api/health`);
|
||||||
@@ -120,7 +183,18 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Import types
|
// Import types
|
||||||
import type { Document, Provider, Model, Source, SystemConfig } from '@/types';
|
import type {
|
||||||
|
Document,
|
||||||
|
Provider,
|
||||||
|
Model,
|
||||||
|
Source,
|
||||||
|
SystemConfig,
|
||||||
|
Notebook,
|
||||||
|
IndexedNotebook,
|
||||||
|
SyncStatus,
|
||||||
|
SyncResponse,
|
||||||
|
NotebookLMSource
|
||||||
|
} from '@/types';
|
||||||
|
|
||||||
export const apiClient = new ApiClient();
|
export const apiClient = new ApiClient();
|
||||||
export default apiClient;
|
export default apiClient;
|
||||||
@@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import {
|
import {
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
FileText,
|
FileText,
|
||||||
|
BookOpen,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Settings,
|
Settings,
|
||||||
Menu,
|
Menu,
|
||||||
@@ -23,6 +24,7 @@ interface LayoutProps {
|
|||||||
const navigation = [
|
const navigation = [
|
||||||
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
|
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
|
||||||
{ name: 'Documents', href: '/documents', icon: FileText },
|
{ name: 'Documents', href: '/documents', icon: FileText },
|
||||||
|
{ name: 'Notebooks', href: '/notebooks', icon: BookOpen },
|
||||||
{ name: 'Chat', href: '/chat', icon: MessageSquare },
|
{ name: 'Chat', href: '/chat', icon: MessageSquare },
|
||||||
{ name: 'Settings', href: '/settings', icon: Settings },
|
{ name: 'Settings', href: '/settings', icon: Settings },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -5,18 +5,26 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { apiClient } from '@/api/client';
|
import { apiClient } from '@/api/client';
|
||||||
import { useChatStore } from '@/stores/chatStore';
|
import { useChatStore } from '@/stores/chatStore';
|
||||||
import { useSettingsStore } from '@/stores/settingsStore';
|
import { useSettingsStore } from '@/stores/settingsStore';
|
||||||
|
import { useNotebookStore } from '@/stores/notebookStore';
|
||||||
import {
|
import {
|
||||||
Send,
|
Send,
|
||||||
Loader2,
|
Loader2,
|
||||||
Bot,
|
Bot,
|
||||||
User,
|
User,
|
||||||
FileText,
|
FileText,
|
||||||
Plus
|
Plus,
|
||||||
|
BookOpen,
|
||||||
|
AlertCircle,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
Database,
|
||||||
|
Layers
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type { Message, Source } from '@/types';
|
import type { Message, Source, NotebookLMSource } from '@/types';
|
||||||
|
|
||||||
export function Chat() {
|
export function Chat() {
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
|
const [showNotebookSelector, setShowNotebookSelector] = useState(false);
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -29,11 +37,28 @@ export function Chat() {
|
|||||||
|
|
||||||
const { defaultProvider, defaultModel } = useSettingsStore();
|
const { defaultProvider, defaultModel } = useSettingsStore();
|
||||||
|
|
||||||
|
const {
|
||||||
|
indexedNotebooks,
|
||||||
|
selectedNotebooks,
|
||||||
|
includeDocuments,
|
||||||
|
isLoading: isNotebooksLoading,
|
||||||
|
fetchIndexedNotebooks,
|
||||||
|
toggleNotebookSelection,
|
||||||
|
selectAllIndexed,
|
||||||
|
deselectAll,
|
||||||
|
setIncludeDocuments,
|
||||||
|
} = useNotebookStore();
|
||||||
|
|
||||||
// Auto-scroll to bottom
|
// Auto-scroll to bottom
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
|
// Fetch indexed notebooks on mount
|
||||||
|
useEffect(() => {
|
||||||
|
fetchIndexedNotebooks();
|
||||||
|
}, [fetchIndexedNotebooks]);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!input.trim() || isLoading) return;
|
if (!input.trim() || isLoading) return;
|
||||||
@@ -50,16 +75,33 @@ export function Chat() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.query(
|
let response;
|
||||||
userMessage.content,
|
|
||||||
defaultProvider,
|
// If notebooks are selected, use the notebook-aware query
|
||||||
defaultModel
|
if (selectedNotebooks.length > 0) {
|
||||||
);
|
response = await apiClient.queryWithNotebooks(
|
||||||
|
userMessage.content,
|
||||||
|
selectedNotebooks,
|
||||||
|
{
|
||||||
|
includeDocuments,
|
||||||
|
provider: defaultProvider,
|
||||||
|
model: defaultModel,
|
||||||
|
k: 10,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Standard query without notebooks
|
||||||
|
response = await apiClient.query(
|
||||||
|
userMessage.content,
|
||||||
|
defaultProvider,
|
||||||
|
defaultModel
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const assistantMessage: Message = {
|
const assistantMessage: Message = {
|
||||||
id: (Date.now() + 1).toString(),
|
id: (Date.now() + 1).toString(),
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: response.response,
|
content: response.answer || response.response,
|
||||||
sources: response.sources,
|
sources: response.sources,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
@@ -87,6 +129,9 @@ export function Chat() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasNotebooks = indexedNotebooks.length > 0;
|
||||||
|
const allSelected = selectedNotebooks.length === indexedNotebooks.length && indexedNotebooks.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-[calc(100vh-8rem)]">
|
<div className="flex flex-col h-[calc(100vh-8rem)]">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -94,7 +139,7 @@ export function Chat() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Chat</h1>
|
<h1 className="text-3xl font-bold tracking-tight">Chat</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Ask questions about your documents
|
Ask questions about your documents and notebooks
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" onClick={handleNewChat}>
|
<Button variant="outline" onClick={handleNewChat}>
|
||||||
@@ -103,6 +148,123 @@ export function Chat() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Notebook Selector */}
|
||||||
|
{hasNotebooks && (
|
||||||
|
<Card className="mb-4">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-between cursor-pointer"
|
||||||
|
onClick={() => setShowNotebookSelector(!showNotebookSelector)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<BookOpen className="h-5 w-5 text-primary" />
|
||||||
|
<span className="font-medium">
|
||||||
|
{selectedNotebooks.length > 0
|
||||||
|
? `${selectedNotebooks.length} notebook${selectedNotebooks.length > 1 ? 's' : ''} selected`
|
||||||
|
: 'No notebooks selected'
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
{includeDocuments && (
|
||||||
|
<span className="text-sm text-muted-foreground flex items-center">
|
||||||
|
<Layers className="h-3 w-3 mr-1" />
|
||||||
|
+ documents
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{isNotebooksLoading && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||||
|
{showNotebookSelector ? (
|
||||||
|
<ChevronUp className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showNotebookSelector && (
|
||||||
|
<div className="mt-4 space-y-4">
|
||||||
|
{/* Options */}
|
||||||
|
<div className="flex items-center justify-between pb-3 border-b">
|
||||||
|
<label className="flex items-center space-x-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={includeDocuments}
|
||||||
|
onChange={(e) => setIncludeDocuments(e.target.checked)}
|
||||||
|
className="rounded border-gray-300"
|
||||||
|
/>
|
||||||
|
<span className="text-sm">Include uploaded documents</span>
|
||||||
|
</label>
|
||||||
|
<div className="space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={selectAllIndexed}
|
||||||
|
disabled={allSelected}
|
||||||
|
>
|
||||||
|
Select All
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={deselectAll}
|
||||||
|
disabled={selectedNotebooks.length === 0}
|
||||||
|
>
|
||||||
|
Deselect All
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Notebook List */}
|
||||||
|
<div className="space-y-2 max-h-48 overflow-y-auto">
|
||||||
|
{indexedNotebooks.map((notebook) => (
|
||||||
|
<label
|
||||||
|
key={notebook.notebook_id}
|
||||||
|
className="flex items-start space-x-3 p-2 rounded-lg hover:bg-muted cursor-pointer"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedNotebooks.includes(notebook.notebook_id)}
|
||||||
|
onChange={() => toggleNotebookSelection(notebook.notebook_id)}
|
||||||
|
className="mt-1 rounded border-gray-300"
|
||||||
|
/>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="font-medium text-sm truncate">
|
||||||
|
{notebook.notebook_title || 'Untitled Notebook'}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{notebook.sources_count} sources • {notebook.chunks_count} chunks
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Database className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* No Notebooks Warning */}
|
||||||
|
{!hasNotebooks && !isNotebooksLoading && (
|
||||||
|
<Card className="mb-4 border-yellow-200 bg-yellow-50 dark:bg-yellow-900/20">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<AlertCircle className="h-5 w-5 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-sm text-yellow-800 dark:text-yellow-200">
|
||||||
|
No notebooks indexed
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-yellow-700 dark:text-yellow-300 mt-1">
|
||||||
|
Sync your NotebookLM notebooks to search through them.
|
||||||
|
Use the API to sync: POST /api/v1/notebooklm/sync/{'{notebook_id}'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Chat Container */}
|
{/* Chat Container */}
|
||||||
<Card className="flex-1 flex flex-col overflow-hidden">
|
<Card className="flex-1 flex flex-col overflow-hidden">
|
||||||
{/* Messages */}
|
{/* Messages */}
|
||||||
@@ -111,12 +273,15 @@ export function Chat() {
|
|||||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
|
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
|
||||||
<Bot className="h-16 w-16 mb-4 opacity-50" />
|
<Bot className="h-16 w-16 mb-4 opacity-50" />
|
||||||
<p className="text-lg font-medium">Start a conversation</p>
|
<p className="text-lg font-medium">Start a conversation</p>
|
||||||
<p className="text-sm">Ask questions about your uploaded documents</p>
|
<p className="text-sm text-center max-w-md">
|
||||||
|
Ask questions about your uploaded documents
|
||||||
|
{hasNotebooks && ' and synced notebooks'}
|
||||||
|
</p>
|
||||||
<div className="mt-8 grid grid-cols-1 md:grid-cols-2 gap-2 w-full max-w-lg">
|
<div className="mt-8 grid grid-cols-1 md:grid-cols-2 gap-2 w-full max-w-lg">
|
||||||
{[
|
{[
|
||||||
'What are the main topics in my documents?',
|
'What are the main topics?',
|
||||||
'Summarize the key points',
|
'Summarize the key points',
|
||||||
'Find information about...',
|
hasNotebooks ? 'What do my notebooks say about...' : 'Find information about...',
|
||||||
'Explain the concept of...',
|
'Explain the concept of...',
|
||||||
].map((suggestion) => (
|
].map((suggestion) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -150,7 +315,10 @@ export function Chat() {
|
|||||||
<div className="p-4 border-t">
|
<div className="p-4 border-t">
|
||||||
<form onSubmit={handleSubmit} className="flex space-x-2">
|
<form onSubmit={handleSubmit} className="flex space-x-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Type your message..."
|
placeholder={hasNotebooks
|
||||||
|
? "Ask about your documents and notebooks..."
|
||||||
|
: "Ask about your documents..."
|
||||||
|
}
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@@ -166,6 +334,7 @@ export function Chat() {
|
|||||||
</form>
|
</form>
|
||||||
<p className="text-xs text-muted-foreground mt-2 text-center">
|
<p className="text-xs text-muted-foreground mt-2 text-center">
|
||||||
Using {defaultProvider} • {defaultModel}
|
Using {defaultProvider} • {defaultModel}
|
||||||
|
{selectedNotebooks.length > 0 && ` • ${selectedNotebooks.length} notebook${selectedNotebooks.length > 1 ? 's' : ''}`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -224,12 +393,30 @@ function ChatMessage({ message }: { message: Message }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Source Badge Component
|
// Source Badge Component
|
||||||
function SourceBadge({ source }: { source: Source }) {
|
function SourceBadge({ source }: { source: Source | NotebookLMSource }) {
|
||||||
|
// Check if it's a notebook source
|
||||||
|
const isNotebookSource = 'source_type' in source && source.source_type === 'notebooklm';
|
||||||
|
|
||||||
|
if (isNotebookSource) {
|
||||||
|
const notebookSource = source as NotebookLMSource;
|
||||||
|
return (
|
||||||
|
<div className="inline-flex items-center space-x-1 px-2 py-1 rounded bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 text-xs">
|
||||||
|
<BookOpen className="h-3 w-3" />
|
||||||
|
<span className="truncate max-w-[150px]">{notebookSource.source_title}</span>
|
||||||
|
<span className="text-muted-foreground">•</span>
|
||||||
|
<span className="truncate max-w-[100px]">{notebookSource.notebook_title}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular document source
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex items-center space-x-1 px-2 py-1 rounded bg-accent text-accent-foreground text-xs">
|
<div className="inline-flex items-center space-x-1 px-2 py-1 rounded bg-accent text-accent-foreground text-xs">
|
||||||
<FileText className="h-3 w-3" />
|
<FileText className="h-3 w-3" />
|
||||||
<span>{source.document_name}</span>
|
<span>{source.document_name}</span>
|
||||||
<span className="text-muted-foreground">({(source.score * 100).toFixed(0)}%)</span>
|
{'score' in source && (
|
||||||
|
<span className="text-muted-foreground">({(source.score * 100).toFixed(0)}%)</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
367
frontend/src/pages/Notebooks.tsx
Normal file
367
frontend/src/pages/Notebooks.tsx
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { apiClient } from '@/api/client';
|
||||||
|
import { useNotebookStore } from '@/stores/notebookStore';
|
||||||
|
import {
|
||||||
|
BookOpen,
|
||||||
|
RefreshCw,
|
||||||
|
Trash2,
|
||||||
|
Check,
|
||||||
|
AlertCircle,
|
||||||
|
Loader2,
|
||||||
|
Database,
|
||||||
|
FileText,
|
||||||
|
Clock,
|
||||||
|
Plus
|
||||||
|
} from 'lucide-react';
|
||||||
|
import type { Notebook, IndexedNotebook } from '@/types';
|
||||||
|
|
||||||
|
export function Notebooks() {
|
||||||
|
const [activeTab, setActiveTab] = useState<'available' | 'indexed'>('indexed');
|
||||||
|
const [availableNotebooks, setAvailableNotebooks] = useState<Notebook[]>([]);
|
||||||
|
const [isLoadingAvailable, setIsLoadingAvailable] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
indexedNotebooks,
|
||||||
|
isLoading,
|
||||||
|
isSyncing,
|
||||||
|
fetchIndexedNotebooks,
|
||||||
|
syncNotebook,
|
||||||
|
deleteNotebookIndex,
|
||||||
|
} = useNotebookStore();
|
||||||
|
|
||||||
|
// Load indexed notebooks on mount
|
||||||
|
useEffect(() => {
|
||||||
|
fetchIndexedNotebooks();
|
||||||
|
}, [fetchIndexedNotebooks]);
|
||||||
|
|
||||||
|
// Load available notebooks when tab changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTab === 'available') {
|
||||||
|
loadAvailableNotebooks();
|
||||||
|
}
|
||||||
|
}, [activeTab]);
|
||||||
|
|
||||||
|
const loadAvailableNotebooks = async () => {
|
||||||
|
setIsLoadingAvailable(true);
|
||||||
|
try {
|
||||||
|
const notebooks = await apiClient.getNotebooks();
|
||||||
|
setAvailableNotebooks(notebooks);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading available notebooks:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingAvailable(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSync = async (notebookId: string) => {
|
||||||
|
await syncNotebook(notebookId);
|
||||||
|
// Refresh both lists
|
||||||
|
await fetchIndexedNotebooks();
|
||||||
|
if (activeTab === 'available') {
|
||||||
|
await loadAvailableNotebooks();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (notebookId: string) => {
|
||||||
|
if (confirm('Are you sure you want to remove this notebook from the search index?')) {
|
||||||
|
await deleteNotebookIndex(notebookId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">Notebooks</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Manage your NotebookLM notebooks and sync them for search
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button onClick={() => fetchIndexedNotebooks()} disabled={isLoading}>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<RefreshCw className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className="p-3 bg-primary/10 rounded-full">
|
||||||
|
<Database className="h-6 w-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-bold">{indexedNotebooks.length}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">Indexed Notebooks</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className="p-3 bg-blue-100 dark:bg-blue-900/30 rounded-full">
|
||||||
|
<FileText className="h-6 w-6 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-bold">
|
||||||
|
{indexedNotebooks.reduce((acc, n) => acc + n.sources_count, 0)}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">Total Sources</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className="p-3 bg-green-100 dark:bg-green-900/30 rounded-full">
|
||||||
|
<Clock className="h-6 w-6 text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-bold">
|
||||||
|
{indexedNotebooks.reduce((acc, n) => acc + n.chunks_count, 0)}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">Total Chunks</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="flex space-x-1 rounded-lg bg-muted p-1 w-fit">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('indexed')}
|
||||||
|
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||||
|
activeTab === 'indexed'
|
||||||
|
? 'bg-background text-foreground shadow-sm'
|
||||||
|
: 'text-muted-foreground hover:text-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Indexed ({indexedNotebooks.length})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('available')}
|
||||||
|
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||||
|
activeTab === 'available'
|
||||||
|
? 'bg-background text-foreground shadow-sm'
|
||||||
|
: 'text-muted-foreground hover:text-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Available
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
{activeTab === 'indexed' ? (
|
||||||
|
<IndexedNotebooksList
|
||||||
|
notebooks={indexedNotebooks}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
onRefresh={fetchIndexedNotebooks}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<AvailableNotebooksList
|
||||||
|
notebooks={availableNotebooks}
|
||||||
|
isLoading={isLoadingAvailable}
|
||||||
|
indexedIds={new Set(indexedNotebooks.map(n => n.notebook_id))}
|
||||||
|
onSync={handleSync}
|
||||||
|
isSyncing={isSyncing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indexed Notebooks List Component
|
||||||
|
function IndexedNotebooksList({
|
||||||
|
notebooks,
|
||||||
|
isLoading,
|
||||||
|
onDelete,
|
||||||
|
onRefresh
|
||||||
|
}: {
|
||||||
|
notebooks: IndexedNotebook[];
|
||||||
|
isLoading: boolean;
|
||||||
|
onDelete: (id: string) => void;
|
||||||
|
onRefresh: () => void;
|
||||||
|
}) {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notebooks.length === 0) {
|
||||||
|
return (
|
||||||
|
<Card className="border-dashed">
|
||||||
|
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
|
||||||
|
<Database className="h-12 w-12 text-muted-foreground mb-4" />
|
||||||
|
<h3 className="text-lg font-medium mb-2">No notebooks indexed</h3>
|
||||||
|
<p className="text-sm text-muted-foreground max-w-md mb-4">
|
||||||
|
Sync your NotebookLM notebooks to search through them using RAG.
|
||||||
|
Switch to the "Available" tab to see notebooks you can sync.
|
||||||
|
</p>
|
||||||
|
<Button onClick={onRefresh} variant="outline">
|
||||||
|
<RefreshCw className="mr-2 h-4 w-4" />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{notebooks.map((notebook) => (
|
||||||
|
<Card key={notebook.notebook_id} className="hover:shadow-md transition-shadow">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-start space-x-4">
|
||||||
|
<div className="p-2 bg-primary/10 rounded-lg">
|
||||||
|
<BookOpen className="h-6 w-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="font-semibold truncate">
|
||||||
|
{notebook.notebook_title || 'Untitled Notebook'}
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center space-x-4 mt-2 text-sm text-muted-foreground">
|
||||||
|
<span className="flex items-center">
|
||||||
|
<FileText className="h-3 w-3 mr-1" />
|
||||||
|
{notebook.sources_count} sources
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center">
|
||||||
|
<Database className="h-3 w-3 mr-1" />
|
||||||
|
{notebook.chunks_count} chunks
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{notebook.last_sync && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
|
Last synced: {new Date(notebook.last_sync).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => onDelete(notebook.notebook_id)}
|
||||||
|
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Available Notebooks List Component
|
||||||
|
function AvailableNotebooksList({
|
||||||
|
notebooks,
|
||||||
|
isLoading,
|
||||||
|
indexedIds,
|
||||||
|
onSync,
|
||||||
|
isSyncing,
|
||||||
|
}: {
|
||||||
|
notebooks: Notebook[];
|
||||||
|
isLoading: boolean;
|
||||||
|
indexedIds: Set<string>;
|
||||||
|
onSync: (id: string) => void;
|
||||||
|
isSyncing: boolean;
|
||||||
|
}) {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notebooks.length === 0) {
|
||||||
|
return (
|
||||||
|
<Card className="border-dashed">
|
||||||
|
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
|
||||||
|
<AlertCircle className="h-12 w-12 text-muted-foreground mb-4" />
|
||||||
|
<h3 className="text-lg font-medium mb-2">No notebooks found</h3>
|
||||||
|
<p className="text-sm text-muted-foreground max-w-md">
|
||||||
|
No notebooks found in your NotebookLM account.
|
||||||
|
Make sure you're authenticated with NotebookLM.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{notebooks.map((notebook) => {
|
||||||
|
const isIndexed = indexedIds.has(notebook.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={notebook.id} className={isIndexed ? 'border-green-200 bg-green-50/50 dark:bg-green-900/10' : ''}>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-start space-x-4">
|
||||||
|
<div className={`p-2 rounded-lg ${isIndexed ? 'bg-green-100 dark:bg-green-900/30' : 'bg-muted'}`}>
|
||||||
|
{isIndexed ? (
|
||||||
|
<Check className="h-6 w-6 text-green-600 dark:text-green-400" />
|
||||||
|
) : (
|
||||||
|
<BookOpen className="h-6 w-6 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold">{notebook.title}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{notebook.sources_count !== undefined && (
|
||||||
|
<>{notebook.sources_count} sources • </>
|
||||||
|
)}
|
||||||
|
Created {new Date(notebook.created_at).toLocaleDateString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => onSync(notebook.id)}
|
||||||
|
disabled={isIndexed || isSyncing}
|
||||||
|
variant={isIndexed ? 'ghost' : 'default'}
|
||||||
|
>
|
||||||
|
{isIndexed ? (
|
||||||
|
<>
|
||||||
|
<Check className="mr-2 h-4 w-4" />
|
||||||
|
Synced
|
||||||
|
</>
|
||||||
|
) : isSyncing ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Syncing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Sync
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export { useAuthStore } from './authStore';
|
export { useAuthStore } from './authStore';
|
||||||
export { useChatStore } from './chatStore';
|
export { useChatStore } from './chatStore';
|
||||||
export { useSettingsStore } from './settingsStore';
|
export { useSettingsStore } from './settingsStore';
|
||||||
|
export { useNotebookStore } from './notebookStore';
|
||||||
160
frontend/src/stores/notebookStore.ts
Normal file
160
frontend/src/stores/notebookStore.ts
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { apiClient } from '@/api/client';
|
||||||
|
import type { Notebook, IndexedNotebook, SyncStatus } from '@/types';
|
||||||
|
|
||||||
|
interface NotebookState {
|
||||||
|
// State
|
||||||
|
notebooks: Notebook[];
|
||||||
|
indexedNotebooks: IndexedNotebook[];
|
||||||
|
selectedNotebooks: string[];
|
||||||
|
syncStatus: Map<string, SyncStatus>;
|
||||||
|
isLoading: boolean;
|
||||||
|
isSyncing: boolean;
|
||||||
|
error: string | null;
|
||||||
|
includeDocuments: boolean;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
fetchNotebooks: () => Promise<void>;
|
||||||
|
fetchIndexedNotebooks: () => Promise<void>;
|
||||||
|
syncNotebook: (notebookId: string) => Promise<void>;
|
||||||
|
deleteNotebookIndex: (notebookId: string) => Promise<void>;
|
||||||
|
checkSyncStatus: (notebookId: string) => Promise<void>;
|
||||||
|
toggleNotebookSelection: (notebookId: string) => void;
|
||||||
|
selectAllIndexed: () => void;
|
||||||
|
deselectAll: () => void;
|
||||||
|
setIncludeDocuments: (include: boolean) => void;
|
||||||
|
clearError: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useNotebookStore = create<NotebookState>((set, get) => ({
|
||||||
|
// Initial state
|
||||||
|
notebooks: [],
|
||||||
|
indexedNotebooks: [],
|
||||||
|
selectedNotebooks: [],
|
||||||
|
syncStatus: new Map(),
|
||||||
|
isLoading: false,
|
||||||
|
isSyncing: false,
|
||||||
|
error: null,
|
||||||
|
includeDocuments: true,
|
||||||
|
|
||||||
|
// Fetch all notebooks from NotebookLM
|
||||||
|
fetchNotebooks: async () => {
|
||||||
|
set({ isLoading: true, error: null });
|
||||||
|
try {
|
||||||
|
const notebooks = await apiClient.getNotebooks();
|
||||||
|
set({ notebooks });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching notebooks:', error);
|
||||||
|
set({ error: 'Failed to fetch notebooks' });
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Fetch indexed notebooks
|
||||||
|
fetchIndexedNotebooks: async () => {
|
||||||
|
set({ isLoading: true, error: null });
|
||||||
|
try {
|
||||||
|
const response = await apiClient.getIndexedNotebooks();
|
||||||
|
set({ indexedNotebooks: response.notebooks });
|
||||||
|
|
||||||
|
// Automatically select all indexed notebooks by default
|
||||||
|
if (get().selectedNotebooks.length === 0 && response.notebooks.length > 0) {
|
||||||
|
set({
|
||||||
|
selectedNotebooks: response.notebooks.map(n => n.notebook_id)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching indexed notebooks:', error);
|
||||||
|
set({ error: 'Failed to fetch indexed notebooks' });
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Sync a notebook
|
||||||
|
syncNotebook: async (notebookId: string) => {
|
||||||
|
set({ isSyncing: true, error: null });
|
||||||
|
try {
|
||||||
|
await apiClient.syncNotebook(notebookId);
|
||||||
|
// Refresh indexed notebooks after sync
|
||||||
|
await get().fetchIndexedNotebooks();
|
||||||
|
// Check status
|
||||||
|
await get().checkSyncStatus(notebookId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error syncing notebook:', error);
|
||||||
|
set({ error: 'Failed to sync notebook' });
|
||||||
|
} finally {
|
||||||
|
set({ isSyncing: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Delete notebook index
|
||||||
|
deleteNotebookIndex: async (notebookId: string) => {
|
||||||
|
set({ isLoading: true, error: null });
|
||||||
|
try {
|
||||||
|
await apiClient.deleteNotebookIndex(notebookId);
|
||||||
|
// Remove from selected if present
|
||||||
|
set((state) => ({
|
||||||
|
selectedNotebooks: state.selectedNotebooks.filter(id => id !== notebookId),
|
||||||
|
indexedNotebooks: state.indexedNotebooks.filter(n => n.notebook_id !== notebookId),
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting notebook index:', error);
|
||||||
|
set({ error: 'Failed to delete notebook index' });
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Check sync status
|
||||||
|
checkSyncStatus: async (notebookId: string) => {
|
||||||
|
try {
|
||||||
|
const status = await apiClient.getSyncStatus(notebookId);
|
||||||
|
set((state) => ({
|
||||||
|
syncStatus: new Map(state.syncStatus).set(notebookId, status),
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking sync status:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Toggle notebook selection
|
||||||
|
toggleNotebookSelection: (notebookId: string) => {
|
||||||
|
set((state) => {
|
||||||
|
const isSelected = state.selectedNotebooks.includes(notebookId);
|
||||||
|
if (isSelected) {
|
||||||
|
return {
|
||||||
|
selectedNotebooks: state.selectedNotebooks.filter(id => id !== notebookId),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
selectedNotebooks: [...state.selectedNotebooks, notebookId],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Select all indexed notebooks
|
||||||
|
selectAllIndexed: () => {
|
||||||
|
const { indexedNotebooks } = get();
|
||||||
|
set({
|
||||||
|
selectedNotebooks: indexedNotebooks.map(n => n.notebook_id),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Deselect all notebooks
|
||||||
|
deselectAll: () => {
|
||||||
|
set({ selectedNotebooks: [] });
|
||||||
|
},
|
||||||
|
|
||||||
|
// Set include documents option
|
||||||
|
setIncludeDocuments: (include: boolean) => {
|
||||||
|
set({ includeDocuments: include });
|
||||||
|
},
|
||||||
|
|
||||||
|
// Clear error
|
||||||
|
clearError: () => {
|
||||||
|
set({ error: null });
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -73,3 +73,60 @@ export interface SystemConfig {
|
|||||||
qdrant_host: string;
|
qdrant_host: string;
|
||||||
qdrant_port: number;
|
qdrant_port: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NotebookLM Integration types
|
||||||
|
export interface Notebook {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
sources_count?: number;
|
||||||
|
artifacts_count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotebookSource {
|
||||||
|
id: string;
|
||||||
|
notebook_id: string;
|
||||||
|
type: 'url' | 'file' | 'youtube' | 'drive';
|
||||||
|
title: string;
|
||||||
|
url?: string;
|
||||||
|
status: 'processing' | 'ready' | 'error';
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IndexedNotebook {
|
||||||
|
notebook_id: string;
|
||||||
|
notebook_title: string | null;
|
||||||
|
sources_count: number;
|
||||||
|
chunks_count: number;
|
||||||
|
last_sync: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncStatus {
|
||||||
|
notebook_id: string;
|
||||||
|
status: 'indexed' | 'not_indexed' | 'syncing';
|
||||||
|
sources_count?: number;
|
||||||
|
chunks_count?: number;
|
||||||
|
last_sync?: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncResponse {
|
||||||
|
sync_id: string;
|
||||||
|
notebook_id: string;
|
||||||
|
notebook_title: string | null;
|
||||||
|
status: string;
|
||||||
|
sources_indexed: number;
|
||||||
|
total_chunks: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extended Source type for notebook sources
|
||||||
|
export interface NotebookLMSource extends Source {
|
||||||
|
source_type: 'notebooklm';
|
||||||
|
notebook_id: string;
|
||||||
|
notebook_title: string;
|
||||||
|
source_id: string;
|
||||||
|
source_title: string;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user