release: 0.1.4 — sidebar time filter + clickable user card

- Sidebar search row gains a recency-filter chip (All / 1d / 3d /
  7d). Active filter highlights brand color; selection persists in
  localStorage (`cc-haha.sidebar.timeFilter`). Default `all` keeps
  current behavior.
- SidebarUserCard becomes a button — clicking the avatar/name pill
  opens https://code.xinghanlab.com/ in the system browser via
  @tauri-apps/plugin-shell, with window.open fallback for dev builds.
  Collapsed-sidebar avatar circle also clickable.
- i18n: `sidebar.timeFilter.title/all` + `sidebar.userCard.openProfile`
  added in zh + en.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 15:53:22 +08:00
co-authored by Claude Opus 4.7
parent 66b47f61be
commit 6c477881c4
6 changed files with 130 additions and 14 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "heicode-desktop",
"private": true,
"version": "0.1.3",
"version": "0.1.4",
"type": "module",
"scripts": {
"dev": "vite",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "heicode-desktop"
version = "0.1.3"
version = "0.1.4"
edition = "2021"
[lib]
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/nicegui/nicegui/main/nicegui/static/tauri-schema-v2.json",
"productName": "HeiCode",
"version": "0.1.3",
"version": "0.1.4",
"identifier": "com.heicode.desktop",
"build": {
"frontendDist": "../dist",
+121 -11
View File
@@ -21,6 +21,30 @@ type TimeGroup = 'within1day' | 'within3days' | 'within7days' | 'older'
const TIME_GROUP_ORDER: TimeGroup[] = ['within1day', 'within3days', 'within7days', 'older']
// User-selectable time filter for the sidebar history list. 'all' means
// no filtering — all groups visible. Persisted in localStorage so a chosen
// window survives reloads.
type TimeFilter = 'all' | '1d' | '3d' | '7d'
const TIME_FILTER_OPTIONS: TimeFilter[] = ['all', '1d', '3d', '7d']
const TIME_FILTER_KEY = 'cc-haha.sidebar.timeFilter'
function loadTimeFilter(): TimeFilter {
if (typeof window === 'undefined') return 'all'
const v = window.localStorage.getItem(TIME_FILTER_KEY)
return TIME_FILTER_OPTIONS.includes(v as TimeFilter) ? (v as TimeFilter) : 'all'
}
function saveTimeFilter(v: TimeFilter) {
if (typeof window === 'undefined') return
try { window.localStorage.setItem(TIME_FILTER_KEY, v) } catch { /* quota */ }
}
function timeFilterCutoffMs(filter: TimeFilter): number | null {
if (filter === 'all') return null
const days = filter === '1d' ? 1 : filter === '3d' ? 3 : 7
return Date.now() - days * 86400000
}
export function Sidebar() {
const sessions = useSessionStore((s) => s.sessions)
const selectedProjects = useSessionStore((s) => s.selectedProjects)
@@ -48,6 +72,9 @@ export function Sidebar() {
const [renamingId, setRenamingId] = useState<string | null>(null)
const [renameValue, setRenameValue] = useState('')
const [isLoggingOut, setIsLoggingOut] = useState(false)
const [timeFilter, setTimeFilter] = useState<TimeFilter>(() => loadTimeFilter())
const [timeFilterOpen, setTimeFilterOpen] = useState(false)
const timeFilterRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
fetchSessions()
@@ -74,8 +101,24 @@ export function Sidebar() {
const q = searchQuery.toLowerCase()
result = result.filter((s) => s.title.toLowerCase().includes(q))
}
const cutoff = timeFilterCutoffMs(timeFilter)
if (cutoff !== null) {
result = result.filter((s) => new Date(s.modifiedAt).getTime() >= cutoff)
}
return result
}, [sessions, selectedProjects, searchQuery])
}, [sessions, selectedProjects, searchQuery, timeFilter])
// Click-outside to close the time-range popover.
useEffect(() => {
if (!timeFilterOpen) return
const onClick = (e: MouseEvent) => {
if (timeFilterRef.current && !timeFilterRef.current.contains(e.target as Node)) {
setTimeFilterOpen(false)
}
}
document.addEventListener('mousedown', onClick)
return () => document.removeEventListener('mousedown', onClick)
}, [timeFilterOpen])
const timeGroups = useMemo(() => groupByTime(filteredSessions), [filteredSessions])
@@ -236,7 +279,7 @@ export function Sidebar() {
className="sidebar-section sidebar-section--visible relative z-20 flex-none px-3 pb-2"
style={{ overflow: 'visible' }}
>
<div className="flex h-9 items-center rounded-[14px] border border-[var(--color-sidebar-search-border)] bg-[var(--color-sidebar-search-bg)] pl-1.5 pr-3 transition-colors focus-within:border-[var(--color-border-focus)]">
<div className="flex h-9 items-center rounded-[14px] border border-[var(--color-sidebar-search-border)] bg-[var(--color-sidebar-search-bg)] pl-1.5 pr-2 transition-colors focus-within:border-[var(--color-border-focus)]">
<ProjectFilter variant="embedded" />
<span className="mx-2 h-4 w-px bg-[var(--color-border)]/80" aria-hidden="true" />
<span className="pointer-events-none flex shrink-0 items-center text-[var(--color-text-tertiary)]">
@@ -250,6 +293,53 @@ export function Sidebar() {
onChange={(e) => setSearchQuery(e.target.value)}
className="min-w-0 flex-1 bg-transparent pl-2 pr-0 text-[13px] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] outline-none"
/>
{/* Time-range filter — opens a popover with All / 1d / 3d / 7d.
The currently active option lights up brand color; a tiny
dot indicates non-default. */}
<div ref={timeFilterRef} className="relative ml-1 flex-shrink-0">
<button
type="button"
onClick={() => setTimeFilterOpen((v) => !v)}
title={t('sidebar.timeFilter.title')}
className={`flex h-7 items-center gap-1 rounded-full px-2 text-[11px] font-semibold transition-colors ${
timeFilter === 'all'
? 'text-[var(--color-text-tertiary)] hover:bg-[var(--color-surface-container)]'
: 'bg-[var(--color-brand)]/12 text-[var(--color-brand)]'
}`}
>
<span className="material-symbols-outlined text-[14px]">tune</span>
<span>{timeFilter === 'all' ? t('sidebar.timeFilter.all') : timeFilter}</span>
</button>
{timeFilterOpen && (
<div className="absolute right-0 top-9 z-50 w-32 rounded-[10px] border border-[var(--color-border)] bg-[var(--color-surface-container-low)] py-1 shadow-[var(--shadow-dropdown)]">
{TIME_FILTER_OPTIONS.map((opt) => {
const label = opt === 'all' ? t('sidebar.timeFilter.all') : opt
const selected = timeFilter === opt
return (
<button
key={opt}
type="button"
onClick={() => {
setTimeFilter(opt)
saveTimeFilter(opt)
setTimeFilterOpen(false)
}}
className={`flex w-full items-center justify-between px-3 py-1.5 text-[12px] transition-colors ${
selected
? 'text-[var(--color-brand)]'
: 'text-[var(--color-text-primary)] hover:bg-[var(--color-surface-container)]'
}`}
>
<span>{label}</span>
{selected && (
<span className="material-symbols-outlined text-[14px]">check</span>
)}
</button>
)
})}
</div>
)}
</div>
</div>
</div>
@@ -514,8 +604,21 @@ function NavItem({
// Logged-in user card in the bottom sidebar — gradient-filled avatar +
// name + email + role badge. Always visible so the user can see who they
// are signed in as without clicking anywhere. Two layouts: expanded (full
// pill) and collapsed (small avatar circle with title tooltip).
// are signed in as without clicking anywhere. Clicking the card opens the
// account page on Heicode Manager (code.xinghanlab.com) in the system
// browser, so users can manage their balance / API keys / etc. Two
// layouts: expanded (full pill) and collapsed (small avatar circle with
// title tooltip).
const HEICODE_MANAGER_URL = 'https://code.xinghanlab.com/'
function openManagerProfile() {
// Tauri shell plugin in the bundled app; falls back to window.open in
// the dev/preview build where the plugin isn't injected.
import('@tauri-apps/plugin-shell')
.then((mod) => mod.open(HEICODE_MANAGER_URL))
.catch(() => window.open(HEICODE_MANAGER_URL, '_blank'))
}
function SidebarUserCard({
user,
collapsed,
@@ -532,22 +635,29 @@ function SidebarUserCard({
const initial = (name.slice(0, 1) || '?').toUpperCase()
const role = user.role?.trim().toLowerCase()
const roleBadge = role && role !== 'user' && role !== '1' ? role : null
const t = useTranslation()
const profileTitle = t('sidebar.userCard.openProfile')
if (collapsed) {
return (
<div
className="mb-2 flex h-9 w-9 items-center justify-center rounded-full text-[12px] font-bold text-white"
<button
type="button"
onClick={openManagerProfile}
className="mb-2 flex h-9 w-9 cursor-pointer items-center justify-center rounded-full text-[12px] font-bold text-white transition-opacity hover:opacity-85"
style={{ backgroundImage: 'var(--gradient-brand-wordmark)' }}
title={user.email || name}
title={`${profileTitle}\n${user.email || name}`}
>
{initial}
</div>
</button>
)
}
return (
<div
className="mb-2 flex items-center gap-2.5 rounded-[12px] border px-2.5 py-2"
<button
type="button"
onClick={openManagerProfile}
title={profileTitle}
className="mb-2 flex w-full items-center gap-2.5 rounded-[12px] border px-2.5 py-2 text-left transition-colors hover:bg-[rgba(123,107,227,0.10)]"
style={{
borderColor: 'rgba(123,107,227,0.22)',
backgroundColor: 'rgba(123,107,227,0.06)',
@@ -580,7 +690,7 @@ function SidebarUserCard({
{roleBadge}
</span>
)}
</div>
</button>
)
}
+3
View File
@@ -35,6 +35,9 @@ export const en = {
'sidebar.timeGroup.within3days': 'Past 3 days',
'sidebar.timeGroup.within7days': 'Past 7 days',
'sidebar.timeGroup.older': 'All older',
'sidebar.timeFilter.title': 'Filter by recency',
'sidebar.timeFilter.all': 'All',
'sidebar.userCard.openProfile': 'Open account on Heicode Manager',
'sidebar.collapse': 'Collapse sidebar',
'sidebar.expand': 'Expand sidebar',
'sidebar.logout': 'Log out',
+3
View File
@@ -37,6 +37,9 @@ export const zh: Record<TranslationKey, string> = {
'sidebar.timeGroup.within3days': '3 天内',
'sidebar.timeGroup.within7days': '7 天内',
'sidebar.timeGroup.older': '全部',
'sidebar.timeFilter.title': '按时间筛选',
'sidebar.timeFilter.all': '全部',
'sidebar.userCard.openProfile': '在 Heicode Manager 查看账号',
'sidebar.collapse': '折叠侧边栏',
'sidebar.expand': '展开侧边栏',
'sidebar.logout': '退出登录',