Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 6 additions & 18 deletions backend/frontend/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -378,8 +378,8 @@ class OpenNotebook {
this.updateFooter();
}

// 从服务器获取最新数据
const notebooks = await this.api('/notebooks');
// 从服务器获取最新数据(包含统计信息)
const notebooks = await this.api('/notebooks/stats');
this.notebooks = notebooks;

// 更新缓存
Expand Down Expand Up @@ -424,8 +424,10 @@ class OpenNotebook {
card.dataset.id = nb.id;
card.querySelector('.notebook-card-name').textContent = nb.name;
card.querySelector('.notebook-card-desc').textContent = nb.description || '暂无描述';

this.loadNotebookCardCounts(nb.id, card);

// 直接使用从 API 获取的统计信息
card.querySelector('.stat-sources').textContent = `${nb.source_count || 0} 来源`;
card.querySelector('.stat-notes').textContent = `${nb.note_count || 0} 笔记`;

card.addEventListener('click', (e) => {
if (!e.target.closest('.btn-delete-card')) {
Expand All @@ -452,20 +454,6 @@ class OpenNotebook {
});
}

async loadNotebookCardCounts(notebookId, element) {
try {
const [sources, notes] = await Promise.all([
this.api(`/notebooks/${notebookId}/sources`),
this.api(`/notebooks/${notebookId}/notes`)
]);

element.querySelector('.stat-sources').textContent = `${sources.length} 来源`;
element.querySelector('.stat-notes').textContent = `${notes.length} 笔记`;
} catch (error) {
// 忽略错误
}
}

switchView(view) {
const landing = document.getElementById('landingPage');
const workspace = document.getElementById('workspaceContainer');
Expand Down
11 changes: 11 additions & 0 deletions backend/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ func (s *Server) setupRoutes() {
notebooks := api.Group("/notebooks")
{
notebooks.GET("", s.handleListNotebooks)
notebooks.GET("/stats", s.handleListNotebooksWithStats)
notebooks.POST("", s.handleCreateNotebook)
notebooks.GET("/:id", s.handleGetNotebook)
notebooks.PUT("/:id", s.handleUpdateNotebook)
Expand Down Expand Up @@ -209,6 +210,16 @@ func (s *Server) handleListNotebooks(c *gin.Context) {
c.JSON(http.StatusOK, notebooks)
}

func (s *Server) handleListNotebooksWithStats(c *gin.Context) {
ctx := context.Background()
notebooks, err := s.store.ListNotebooksWithStats(ctx)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "Failed to list notebooks with stats"})
return
}
c.JSON(http.StatusOK, notebooks)
}

func (s *Server) handleCreateNotebook(c *gin.Context) {
ctx := context.Background()

Expand Down
42 changes: 42 additions & 0 deletions backend/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,48 @@ func (s *Store) DeleteNotebook(ctx context.Context, id string) error {
return err
}

// ListNotebooksWithStats retrieves all notebooks with their source and note counts
func (s *Store) ListNotebooksWithStats(ctx context.Context) ([]NotebookWithStats, error) {
query := `
SELECT
n.id, n.name, n.description, n.created_at, n.updated_at, n.metadata,
COALESCE((SELECT COUNT(*) FROM sources WHERE notebook_id = n.id), 0) as source_count,
COALESCE((SELECT COUNT(*) FROM notes WHERE notebook_id = n.id), 0) as note_count
FROM notebooks n
ORDER BY n.updated_at DESC
`

rows, err := s.db.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()

notebooks := make([]NotebookWithStats, 0)
for rows.Next() {
var nb NotebookWithStats
var metadataJSON string
var createdAt, updatedAt int64

if err := rows.Scan(&nb.ID, &nb.Name, &nb.Description, &createdAt, &updatedAt, &metadataJSON, &nb.SourceCount, &nb.NoteCount); err != nil {
return nil, err
}

nb.CreatedAt = time.Unix(createdAt, 0)
nb.UpdatedAt = time.Unix(updatedAt, 0)

if metadataJSON != "" {
json.Unmarshal([]byte(metadataJSON), &nb.Metadata)
} else {
nb.Metadata = make(map[string]interface{})
}

notebooks = append(notebooks, nb)
}

return notebooks, nil
}

// Source operations

// CreateSource creates a new source
Expand Down
12 changes: 12 additions & 0 deletions backend/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ type Notebook struct {
Metadata map[string]interface{} `json:"metadata,omitempty"`
}

// NotebookWithStats represents a notebook with statistics
type NotebookWithStats struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
SourceCount int `json:"source_count"`
NoteCount int `json:"note_count"`
}

// ChatMessage represents a chat message
type ChatMessage struct {
ID string `json:"id"`
Expand Down