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
4 changes: 4 additions & 0 deletions controllers/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ func (c *ApiController) GetHubStores() {
c.ResponseError(err.Error())
return
}
if err := object.FillStoreFavoriteCounts(stores); err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(object.GetMaskedStores(stores, c.GetSessionUser()))
}

Expand Down
6 changes: 6 additions & 0 deletions controllers/store_favorite.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,16 @@ func (c *ApiController) GetStoreFavoriteStatus() {
c.ResponseError(err.Error())
return
}
forkCount, err := object.GetStoreForkCount(storeOwner, storeName)
if err != nil {
c.ResponseError(err.Error())
return
}

result := map[string]interface{}{
"starCount": starCount,
"watchCount": watchCount,
"forkCount": forkCount,
"starred": false,
"watched": false,
"hasForked": false,
Expand Down
3 changes: 3 additions & 0 deletions object/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ type Store struct {
ChatCount int `xorm:"-" json:"chatCount"`
MessageCount int `xorm:"-" json:"messageCount"`
VectorCount int `xorm:"-" json:"vectorCount"`
StarCount int `xorm:"-" json:"starCount"`
WatchCount int `xorm:"-" json:"watchCount"`
ForkCount int `xorm:"-" json:"forkCount"`
HubDbName string `xorm:"-" json:"hubDbName"`
Endpoint string `xorm:"-" json:"endpoint"`

Expand Down
68 changes: 68 additions & 0 deletions object/store_favorite.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,71 @@ func GetFavoredStores(user, favoriteType string) ([]*Store, error) {
}
return stores, nil
}

// FillStoreFavoriteCounts populates StarCount / WatchCount / ForkCount on the
// given stores using grouped queries (star/watch from store_favorite, fork from
// the store table's forked_from columns) — avoids N+1 for hub/list rendering.
func FillStoreFavoriteCounts(stores []*Store) error {
if len(stores) == 0 {
return nil
}

type favoriteCountRow struct {
Type string
StoreOwner string
StoreName string
Count int
}
favoriteRows := []favoriteCountRow{}
err := adapter.engine.Table(new(StoreFavorite)).
Select("type, store_owner, store_name, count(*) as count").
GroupBy("type, store_owner, store_name").
Find(&favoriteRows)
if err != nil {
return err
}

starMap := map[string]int{}
watchMap := map[string]int{}
for _, row := range favoriteRows {
key := row.StoreOwner + "/" + row.StoreName
if row.Type == FavoriteTypeStar {
starMap[key] = row.Count
} else if row.Type == FavoriteTypeWatch {
watchMap[key] = row.Count
}
}

type forkCountRow struct {
ForkedFromOwner string
ForkedFromName string
Count int
}
forkRows := []forkCountRow{}
err = adapter.engine.Table(new(Store)).
Select("forked_from_owner, forked_from_name, count(*) as count").
Where("forked_from_owner <> ? and forked_from_name <> ?", "", "").
GroupBy("forked_from_owner, forked_from_name").
Find(&forkRows)
if err != nil {
return err
}

forkMap := map[string]int{}
for _, row := range forkRows {
forkMap[row.ForkedFromOwner+"/"+row.ForkedFromName] = row.Count
}

for _, store := range stores {
key := store.Owner + "/" + store.Name
store.StarCount = starMap[key]
store.WatchCount = watchMap[key]
store.ForkCount = forkMap[key]
}
return nil
}

// GetStoreForkCount returns how many stores were forked from the given store.
func GetStoreForkCount(owner, name string) (int64, error) {
return adapter.engine.Where("forked_from_owner = ? and forked_from_name = ?", owner, name).Count(&Store{})
}
1 change: 1 addition & 0 deletions web/src/StoreHubAgentDetail.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ function renderHeader(store, onStartChat, onFork, forking, favoriteStatus, starL
<Tooltip title={forkDisabledReason}>
<Button icon={<ForkOutlined />} loading={forking} disabled={Boolean(forkDisabledReason)} onClick={onFork}>
{i18next.t("store:Fork")}
{status.forkCount > 0 ? ` (${status.forkCount})` : ""}
</Button>
</Tooltip>
<Button type="primary" icon={<CommentOutlined />} onClick={onStartChat}>
Expand Down
31 changes: 23 additions & 8 deletions web/src/StoreHubPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import React from "react";
import {Avatar, Button, Card, Col, Empty, Input, Row, Segmented, Select, Spin, Tag, Tooltip, Typography} from "antd";
import {CopyOutlined, InfoCircleOutlined, LinkOutlined, RobotOutlined, SortAscendingOutlined, SortDescendingOutlined} from "@ant-design/icons";
import {CopyOutlined, EyeOutlined, ForkOutlined, InfoCircleOutlined, LinkOutlined, RobotOutlined, SortAscendingOutlined, SortDescendingOutlined, StarOutlined} from "@ant-design/icons";
import * as StoreBackend from "./backend/StoreBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
Expand All @@ -37,8 +37,8 @@ class StoreHubPage extends React.Component {
filterSubject: "",
filterGrade: "",
filterTopic: "",
sortField: "",
sortOrder: "asc",
sortField: "starCount",
sortOrder: "desc",
};
}

Expand Down Expand Up @@ -116,7 +116,13 @@ class StoreHubPage extends React.Component {
}

if (sortField) {
const numericFields = ["starCount", "watchCount", "forkCount"];
result.sort((a, b) => {
if (numericFields.includes(sortField)) {
const va = a[sortField] || 0;
const vb = b[sortField] || 0;
return sortOrder === "asc" ? va - vb : vb - va;
}
let va, vb;
if (sortField === "displayName") {
va = (a.displayName || a.name || "").toLowerCase();
Expand All @@ -138,8 +144,10 @@ class StoreHubPage extends React.Component {
}

hasActiveFilters() {
const {searchText, filterSubject, filterGrade, filterTopic, sortField} = this.state;
return !!(searchText || filterSubject || filterGrade || filterTopic || sortField);
const {searchText, filterSubject, filterGrade, filterTopic, sortField, sortOrder} = this.state;
// The default "most starred" sort is not considered an active filter.
const nonDefaultSort = sortField !== "starCount" || sortOrder !== "desc";
return !!(searchText || filterSubject || filterGrade || filterTopic || nonDefaultSort);
}

resetFilters() {
Expand All @@ -148,8 +156,8 @@ class StoreHubPage extends React.Component {
filterSubject: "",
filterGrade: "",
filterTopic: "",
sortField: "",
sortOrder: "asc",
sortField: "starCount",
sortOrder: "desc",
});
}

Expand Down Expand Up @@ -196,7 +204,9 @@ class StoreHubPage extends React.Component {
const isFiltered = this.hasActiveFilters();

const sortFieldOptions = [
{value: "", label: i18next.t("general:Sort")},
{value: "starCount", label: i18next.t("store:Stars")},
{value: "watchCount", label: i18next.t("store:Watchers")},
{value: "forkCount", label: i18next.t("store:Forks")},
{value: "displayName", label: i18next.t("general:Display name")},
{value: "author", label: i18next.t("general:Author")},
{value: "affiliation", label: i18next.t("store:Affiliation")},
Expand Down Expand Up @@ -350,6 +360,11 @@ class StoreHubPage extends React.Component {
) : (
<div style={{height: 60}} />
)}
<div style={{display: "flex", alignItems: "center", gap: 14, marginBottom: 10, color: "var(--ant-color-text-secondary)", fontSize: 12}}>
<Tooltip title={i18next.t("store:Stars")}><span><StarOutlined /> {store.starCount || 0}</span></Tooltip>
<Tooltip title={i18next.t("store:Watchers")}><span><EyeOutlined /> {store.watchCount || 0}</span></Tooltip>
<Tooltip title={i18next.t("store:Forks")}><span><ForkOutlined /> {store.forkCount || 0}</span></Tooltip>
</div>
<div
style={{
display: "flex",
Expand Down
3 changes: 3 additions & 0 deletions web/src/locales/en/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,7 @@
"Fork failed": "Fork failed",
"Forked from": "Forked from",
"Forked successfully": "Forked successfully",
"Forks": "Forks",
"Frequency": "Frequency",
"Frequency - Tooltip": "Max API calls per minute",
"Grade": "Grade",
Expand Down Expand Up @@ -884,6 +885,7 @@
"Split provider - Tooltip": "Text splitting strategy for document processing",
"Star": "Star",
"Starred": "Starred",
"Stars": "Stars",
"Start Chat": "Start Chat",
"Storage": "Storage",
"Storage provider": "Storage provider",
Expand Down Expand Up @@ -921,6 +923,7 @@
"View in Hub": "View in Hub",
"Views": "Views",
"Watch": "Watch",
"Watchers": "Watchers",
"Watching": "Watching",
"Welcome": "Welcome",
"Welcome - Tooltip": "Welcome message",
Expand Down
3 changes: 3 additions & 0 deletions web/src/locales/zh/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,7 @@
"Fork failed": "复刻失败",
"Forked from": "复刻来源",
"Forked successfully": "复刻成功",
"Forks": "复刻",
"Frequency": "频率",
"Frequency - Tooltip": "AI模型调用频率限制(次/分钟)",
"Grade": "年级",
Expand Down Expand Up @@ -884,6 +885,7 @@
"Split provider - Tooltip": "文本分割策略",
"Star": "点赞",
"Starred": "已点赞",
"Stars": "点赞",
"Start Chat": "开始对话",
"Storage": "存储",
"Storage provider": "存储提供商",
Expand Down Expand Up @@ -921,6 +923,7 @@
"View in Hub": "在广场中查看",
"Views": "访问量",
"Watch": "关注",
"Watchers": "关注",
"Watching": "已关注",
"Welcome": "欢迎提示词",
"Welcome - Tooltip": "用户首次进入聊天时显示的欢迎语",
Expand Down
Loading