Skip to content

Commit fc1fe83

Browse files
feat: show star/watch/fork counts on the agent hub and sort by them (#2435)
1 parent f3fb35d commit fc1fe83

8 files changed

Lines changed: 111 additions & 8 deletions

File tree

controllers/store.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ func (c *ApiController) GetHubStores() {
3737
c.ResponseError(err.Error())
3838
return
3939
}
40+
if err := object.FillStoreFavoriteCounts(stores); err != nil {
41+
c.ResponseError(err.Error())
42+
return
43+
}
4044
c.ResponseOk(object.GetMaskedStores(stores, c.GetSessionUser()))
4145
}
4246

controllers/store_favorite.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,16 @@ func (c *ApiController) GetStoreFavoriteStatus() {
122122
c.ResponseError(err.Error())
123123
return
124124
}
125+
forkCount, err := object.GetStoreForkCount(storeOwner, storeName)
126+
if err != nil {
127+
c.ResponseError(err.Error())
128+
return
129+
}
125130

126131
result := map[string]interface{}{
127132
"starCount": starCount,
128133
"watchCount": watchCount,
134+
"forkCount": forkCount,
129135
"starred": false,
130136
"watched": false,
131137
"hasForked": false,

object/store.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ type Store struct {
124124
ChatCount int `xorm:"-" json:"chatCount"`
125125
MessageCount int `xorm:"-" json:"messageCount"`
126126
VectorCount int `xorm:"-" json:"vectorCount"`
127+
StarCount int `xorm:"-" json:"starCount"`
128+
WatchCount int `xorm:"-" json:"watchCount"`
129+
ForkCount int `xorm:"-" json:"forkCount"`
127130
HubDbName string `xorm:"-" json:"hubDbName"`
128131
Endpoint string `xorm:"-" json:"endpoint"`
129132

object/store_favorite.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,71 @@ func GetFavoredStores(user, favoriteType string) ([]*Store, error) {
119119
}
120120
return stores, nil
121121
}
122+
123+
// FillStoreFavoriteCounts populates StarCount / WatchCount / ForkCount on the
124+
// given stores using grouped queries (star/watch from store_favorite, fork from
125+
// the store table's forked_from columns) — avoids N+1 for hub/list rendering.
126+
func FillStoreFavoriteCounts(stores []*Store) error {
127+
if len(stores) == 0 {
128+
return nil
129+
}
130+
131+
type favoriteCountRow struct {
132+
Type string
133+
StoreOwner string
134+
StoreName string
135+
Count int
136+
}
137+
favoriteRows := []favoriteCountRow{}
138+
err := adapter.engine.Table(new(StoreFavorite)).
139+
Select("type, store_owner, store_name, count(*) as count").
140+
GroupBy("type, store_owner, store_name").
141+
Find(&favoriteRows)
142+
if err != nil {
143+
return err
144+
}
145+
146+
starMap := map[string]int{}
147+
watchMap := map[string]int{}
148+
for _, row := range favoriteRows {
149+
key := row.StoreOwner + "/" + row.StoreName
150+
if row.Type == FavoriteTypeStar {
151+
starMap[key] = row.Count
152+
} else if row.Type == FavoriteTypeWatch {
153+
watchMap[key] = row.Count
154+
}
155+
}
156+
157+
type forkCountRow struct {
158+
ForkedFromOwner string
159+
ForkedFromName string
160+
Count int
161+
}
162+
forkRows := []forkCountRow{}
163+
err = adapter.engine.Table(new(Store)).
164+
Select("forked_from_owner, forked_from_name, count(*) as count").
165+
Where("forked_from_owner <> ? and forked_from_name <> ?", "", "").
166+
GroupBy("forked_from_owner, forked_from_name").
167+
Find(&forkRows)
168+
if err != nil {
169+
return err
170+
}
171+
172+
forkMap := map[string]int{}
173+
for _, row := range forkRows {
174+
forkMap[row.ForkedFromOwner+"/"+row.ForkedFromName] = row.Count
175+
}
176+
177+
for _, store := range stores {
178+
key := store.Owner + "/" + store.Name
179+
store.StarCount = starMap[key]
180+
store.WatchCount = watchMap[key]
181+
store.ForkCount = forkMap[key]
182+
}
183+
return nil
184+
}
185+
186+
// GetStoreForkCount returns how many stores were forked from the given store.
187+
func GetStoreForkCount(owner, name string) (int64, error) {
188+
return adapter.engine.Where("forked_from_owner = ? and forked_from_name = ?", owner, name).Count(&Store{})
189+
}

web/src/StoreHubAgentDetail.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ function renderHeader(store, account, onStartChat, onFork, forking, favoriteStat
9797
<Tooltip title={forkDisabledReason}>
9898
<Button icon={<ForkOutlined />} loading={forking} disabled={Boolean(forkDisabledReason)} onClick={onFork}>
9999
{i18next.t("store:Fork")}
100+
{status.forkCount > 0 ? ` (${status.forkCount})` : ""}
100101
</Button>
101102
</Tooltip>
102103
<Button type="primary" icon={<CommentOutlined />} onClick={onStartChat}>

web/src/StoreHubPage.js

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import React from "react";
1616
import {Avatar, Button, Card, Col, Empty, Input, Row, Segmented, Select, Spin, Tag, Tooltip, Typography} from "antd";
17-
import {CopyOutlined, InfoCircleOutlined, LinkOutlined, RobotOutlined, SortAscendingOutlined, SortDescendingOutlined} from "@ant-design/icons";
17+
import {CopyOutlined, EyeOutlined, ForkOutlined, InfoCircleOutlined, LinkOutlined, RobotOutlined, SortAscendingOutlined, SortDescendingOutlined, StarOutlined} from "@ant-design/icons";
1818
import * as StoreBackend from "./backend/StoreBackend";
1919
import * as Setting from "./Setting";
2020
import i18next from "i18next";
@@ -38,8 +38,8 @@ class StoreHubPage extends React.Component {
3838
filterSubject: "",
3939
filterGrade: "",
4040
filterTopic: "",
41-
sortField: "",
42-
sortOrder: "asc",
41+
sortField: "starCount",
42+
sortOrder: "desc",
4343
};
4444
}
4545

@@ -117,7 +117,13 @@ class StoreHubPage extends React.Component {
117117
}
118118

119119
if (sortField) {
120+
const numericFields = ["starCount", "watchCount", "forkCount"];
120121
result.sort((a, b) => {
122+
if (numericFields.includes(sortField)) {
123+
const va = a[sortField] || 0;
124+
const vb = b[sortField] || 0;
125+
return sortOrder === "asc" ? va - vb : vb - va;
126+
}
121127
let va, vb;
122128
if (sortField === "displayName") {
123129
va = (a.displayName || a.name || "").toLowerCase();
@@ -139,8 +145,10 @@ class StoreHubPage extends React.Component {
139145
}
140146

141147
hasActiveFilters() {
142-
const {searchText, filterSubject, filterGrade, filterTopic, sortField} = this.state;
143-
return !!(searchText || filterSubject || filterGrade || filterTopic || sortField);
148+
const {searchText, filterSubject, filterGrade, filterTopic, sortField, sortOrder} = this.state;
149+
// The default "most starred" sort is not considered an active filter.
150+
const nonDefaultSort = sortField !== "starCount" || sortOrder !== "desc";
151+
return !!(searchText || filterSubject || filterGrade || filterTopic || nonDefaultSort);
144152
}
145153

146154
resetFilters() {
@@ -149,8 +157,8 @@ class StoreHubPage extends React.Component {
149157
filterSubject: "",
150158
filterGrade: "",
151159
filterTopic: "",
152-
sortField: "",
153-
sortOrder: "asc",
160+
sortField: "starCount",
161+
sortOrder: "desc",
154162
});
155163
}
156164

@@ -197,7 +205,9 @@ class StoreHubPage extends React.Component {
197205
const isFiltered = this.hasActiveFilters();
198206

199207
const sortFieldOptions = [
200-
{value: "", label: i18next.t("general:Sort")},
208+
{value: "starCount", label: i18next.t("store:Stars")},
209+
{value: "watchCount", label: i18next.t("store:Watchers")},
210+
{value: "forkCount", label: i18next.t("store:Forks")},
201211
{value: "displayName", label: i18next.t("general:Display name")},
202212
{value: "author", label: i18next.t("general:Author")},
203213
{value: "affiliation", label: i18next.t("store:Affiliation")},
@@ -354,6 +364,11 @@ class StoreHubPage extends React.Component {
354364
) : (
355365
<div style={{height: 60}} />
356366
)}
367+
<div style={{display: "flex", alignItems: "center", gap: 14, marginBottom: 10, color: "var(--ant-color-text-secondary)", fontSize: 12}}>
368+
<Tooltip title={i18next.t("store:Stars")}><span><StarOutlined /> {store.starCount || 0}</span></Tooltip>
369+
<Tooltip title={i18next.t("store:Watchers")}><span><EyeOutlined /> {store.watchCount || 0}</span></Tooltip>
370+
<Tooltip title={i18next.t("store:Forks")}><span><ForkOutlined /> {store.forkCount || 0}</span></Tooltip>
371+
</div>
357372
<div
358373
style={{
359374
display: "flex",

web/src/locales/en/data.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,7 @@
802802
"Fork failed": "Fork failed",
803803
"Forked from": "Forked from",
804804
"Forked successfully": "Forked successfully",
805+
"Forks": "Forks",
805806
"Frequency": "Frequency",
806807
"Frequency - Tooltip": "Max API calls per minute",
807808
"Grade": "Grade",
@@ -884,6 +885,7 @@
884885
"Split provider - Tooltip": "Text splitting strategy for document processing",
885886
"Star": "Star",
886887
"Starred": "Starred",
888+
"Stars": "Stars",
887889
"Start Chat": "Start Chat",
888890
"Storage": "Storage",
889891
"Storage provider": "Storage provider",
@@ -921,6 +923,7 @@
921923
"View in Hub": "View in Hub",
922924
"Views": "Views",
923925
"Watch": "Watch",
926+
"Watchers": "Watchers",
924927
"Watching": "Watching",
925928
"Welcome": "Welcome",
926929
"Welcome - Tooltip": "Welcome message",

web/src/locales/zh/data.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,7 @@
802802
"Fork failed": "复刻失败",
803803
"Forked from": "复刻来源",
804804
"Forked successfully": "复刻成功",
805+
"Forks": "复刻",
805806
"Frequency": "频率",
806807
"Frequency - Tooltip": "AI模型调用频率限制(次/分钟)",
807808
"Grade": "年级",
@@ -884,6 +885,7 @@
884885
"Split provider - Tooltip": "文本分割策略",
885886
"Star": "点赞",
886887
"Starred": "已点赞",
888+
"Stars": "点赞",
887889
"Start Chat": "开始对话",
888890
"Storage": "存储",
889891
"Storage provider": "存储提供商",
@@ -921,6 +923,7 @@
921923
"View in Hub": "在广场中查看",
922924
"Views": "访问量",
923925
"Watch": "关注",
926+
"Watchers": "关注",
924927
"Watching": "已关注",
925928
"Welcome": "欢迎提示词",
926929
"Welcome - Tooltip": "用户首次进入聊天时显示的欢迎语",

0 commit comments

Comments
 (0)