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", "name": "heicode-desktop",
"private": true, "private": true,
"version": "0.1.3", "version": "0.1.4",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "heicode-desktop" name = "heicode-desktop"
version = "0.1.3" version = "0.1.4"
edition = "2021" edition = "2021"
[lib] [lib]
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://raw.githubusercontent.com/nicegui/nicegui/main/nicegui/static/tauri-schema-v2.json", "$schema": "https://raw.githubusercontent.com/nicegui/nicegui/main/nicegui/static/tauri-schema-v2.json",
"productName": "HeiCode", "productName": "HeiCode",
"version": "0.1.3", "version": "0.1.4",
"identifier": "com.heicode.desktop", "identifier": "com.heicode.desktop",
"build": { "build": {
"frontendDist": "../dist", "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'] 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() { export function Sidebar() {
const sessions = useSessionStore((s) => s.sessions) const sessions = useSessionStore((s) => s.sessions)
const selectedProjects = useSessionStore((s) => s.selectedProjects) const selectedProjects = useSessionStore((s) => s.selectedProjects)
@@ -48,6 +72,9 @@ export function Sidebar() {
const [renamingId, setRenamingId] = useState<string | null>(null) const [renamingId, setRenamingId] = useState<string | null>(null)
const [renameValue, setRenameValue] = useState('') const [renameValue, setRenameValue] = useState('')
const [isLoggingOut, setIsLoggingOut] = useState(false) const [isLoggingOut, setIsLoggingOut] = useState(false)
const [timeFilter, setTimeFilter] = useState<TimeFilter>(() => loadTimeFilter())
const [timeFilterOpen, setTimeFilterOpen] = useState(false)
const timeFilterRef = useRef<HTMLDivElement | null>(null)
useEffect(() => { useEffect(() => {
fetchSessions() fetchSessions()
@@ -74,8 +101,24 @@ export function Sidebar() {
const q = searchQuery.toLowerCase() const q = searchQuery.toLowerCase()
result = result.filter((s) => s.title.toLowerCase().includes(q)) 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 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]) 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" className="sidebar-section sidebar-section--visible relative z-20 flex-none px-3 pb-2"
style={{ overflow: 'visible' }} 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" /> <ProjectFilter variant="embedded" />
<span className="mx-2 h-4 w-px bg-[var(--color-border)]/80" aria-hidden="true" /> <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)]"> <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)} 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" 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>
</div> </div>
@@ -514,8 +604,21 @@ function NavItem({
// Logged-in user card in the bottom sidebar — gradient-filled avatar + // Logged-in user card in the bottom sidebar — gradient-filled avatar +
// name + email + role badge. Always visible so the user can see who they // name + email + role badge. Always visible so the user can see who they
// are signed in as without clicking anywhere. Two layouts: expanded (full // are signed in as without clicking anywhere. Clicking the card opens the
// pill) and collapsed (small avatar circle with title tooltip). // 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({ function SidebarUserCard({
user, user,
collapsed, collapsed,
@@ -532,22 +635,29 @@ function SidebarUserCard({
const initial = (name.slice(0, 1) || '?').toUpperCase() const initial = (name.slice(0, 1) || '?').toUpperCase()
const role = user.role?.trim().toLowerCase() const role = user.role?.trim().toLowerCase()
const roleBadge = role && role !== 'user' && role !== '1' ? role : null const roleBadge = role && role !== 'user' && role !== '1' ? role : null
const t = useTranslation()
const profileTitle = t('sidebar.userCard.openProfile')
if (collapsed) { if (collapsed) {
return ( return (
<div <button
className="mb-2 flex h-9 w-9 items-center justify-center rounded-full text-[12px] font-bold text-white" 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)' }} style={{ backgroundImage: 'var(--gradient-brand-wordmark)' }}
title={user.email || name} title={`${profileTitle}\n${user.email || name}`}
> >
{initial} {initial}
</div> </button>
) )
} }
return ( return (
<div <button
className="mb-2 flex items-center gap-2.5 rounded-[12px] border px-2.5 py-2" 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={{ style={{
borderColor: 'rgba(123,107,227,0.22)', borderColor: 'rgba(123,107,227,0.22)',
backgroundColor: 'rgba(123,107,227,0.06)', backgroundColor: 'rgba(123,107,227,0.06)',
@@ -580,7 +690,7 @@ function SidebarUserCard({
{roleBadge} {roleBadge}
</span> </span>
)} )}
</div> </button>
) )
} }
+3
View File
@@ -35,6 +35,9 @@ export const en = {
'sidebar.timeGroup.within3days': 'Past 3 days', 'sidebar.timeGroup.within3days': 'Past 3 days',
'sidebar.timeGroup.within7days': 'Past 7 days', 'sidebar.timeGroup.within7days': 'Past 7 days',
'sidebar.timeGroup.older': 'All older', '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.collapse': 'Collapse sidebar',
'sidebar.expand': 'Expand sidebar', 'sidebar.expand': 'Expand sidebar',
'sidebar.logout': 'Log out', 'sidebar.logout': 'Log out',
+3
View File
@@ -37,6 +37,9 @@ export const zh: Record<TranslationKey, string> = {
'sidebar.timeGroup.within3days': '3 天内', 'sidebar.timeGroup.within3days': '3 天内',
'sidebar.timeGroup.within7days': '7 天内', 'sidebar.timeGroup.within7days': '7 天内',
'sidebar.timeGroup.older': '全部', 'sidebar.timeGroup.older': '全部',
'sidebar.timeFilter.title': '按时间筛选',
'sidebar.timeFilter.all': '全部',
'sidebar.userCard.openProfile': '在 Heicode Manager 查看账号',
'sidebar.collapse': '折叠侧边栏', 'sidebar.collapse': '折叠侧边栏',
'sidebar.expand': '展开侧边栏', 'sidebar.expand': '展开侧边栏',
'sidebar.logout': '退出登录', 'sidebar.logout': '退出登录',