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:
@@ -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,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,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",
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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': '退出登录',
|
||||||
|
|||||||
Reference in New Issue
Block a user