mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-24 16:54:43 +00:00
refine inline message actions: fork icon, sine-wave read-aloud, shared age util
- Use the fork glyph for branch and a sine wave for read aloud (all one lib now) - Extract compact "2h ago" into formatAgo() in lib/time.ts (+ ageDays locale string) - Cover formatAgo with a unit test
This commit is contained in:
parent
e4877ba96e
commit
ca244e2168
9 changed files with 77 additions and 16 deletions
|
|
@ -23,9 +23,9 @@ import { Codicon } from '@/components/ui/codicon'
|
|||
import { CopyButton } from '@/components/ui/copy-button'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { triggerHaptic } from '@/lib/haptics'
|
||||
import { GitBranchIcon, Loader2Icon, Volume2Icon, VolumeXIcon, XIcon } from '@/lib/icons'
|
||||
import { AudioLines, GitForkIcon, Loader2Icon, RefreshCwIcon, VolumeXIcon, XIcon } from '@/lib/icons'
|
||||
import { extractPreviewTargets } from '@/lib/preview-targets'
|
||||
import { relativeTime } from '@/lib/time'
|
||||
import { formatAgo } from '@/lib/time'
|
||||
import { useEnterAnimation } from '@/lib/use-enter-animation'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { playSpeechText, stopVoicePlayback } from '@/lib/voice-playback'
|
||||
|
|
@ -162,13 +162,13 @@ const AssistantActionBar: FC<MessageActionProps> = ({ messageId, getMessageText,
|
|||
}}
|
||||
tooltip={copy.branchNewChat}
|
||||
>
|
||||
<GitBranchIcon className="size-3.5" />
|
||||
<GitForkIcon className="size-3.5" />
|
||||
</TooltipIconButton>
|
||||
<CopyButton appearance="icon" buttonSize="icon" label={copy.copy} text={getMessageText} />
|
||||
<ReadAloudButton getText={getMessageText} messageId={messageId} />
|
||||
<ActionBarPrimitive.Reload asChild>
|
||||
<TooltipIconButton onClick={() => triggerHaptic('submit')} tooltip={copy.refresh}>
|
||||
<Codicon name="refresh" />
|
||||
<RefreshCwIcon className="size-3.5" />
|
||||
</TooltipIconButton>
|
||||
</ActionBarPrimitive.Reload>
|
||||
</ActionBarPrimitive.Root>
|
||||
|
|
@ -187,7 +187,7 @@ const ReadAloudButton: FC<{ getText: () => string; messageId: string }> = ({ get
|
|||
const isPreparing = readAloudStatus === 'preparing'
|
||||
const isSpeaking = readAloudStatus === 'speaking'
|
||||
const anyPlaybackActive = voicePlayback.status !== 'idle'
|
||||
const Icon = isPreparing ? Loader2Icon : isSpeaking ? VolumeXIcon : Volume2Icon
|
||||
const Icon = isPreparing ? Loader2Icon : isSpeaking ? VolumeXIcon : AudioLines
|
||||
const tooltip = isPreparing ? copy.preparingAudio : isSpeaking ? copy.stopReading : copy.readAloud
|
||||
|
||||
const read = useCallback(async () => {
|
||||
|
|
@ -221,22 +221,19 @@ const ReadAloudButton: FC<{ getText: () => string; messageId: string }> = ({ get
|
|||
const MessageAge: FC = () => {
|
||||
const { t } = useI18n()
|
||||
const createdAt = useAuiState(s => s.message.createdAt)
|
||||
const date = createdAt ? new Date(createdAt) : null
|
||||
|
||||
if (!createdAt) {
|
||||
if (!date || Number.isNaN(date.getTime())) {
|
||||
return null
|
||||
}
|
||||
|
||||
const date = createdAt instanceof Date ? createdAt : new Date(createdAt)
|
||||
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return null
|
||||
}
|
||||
|
||||
const absolute = formatMessageTimestamp(date, t.assistant.thread)
|
||||
|
||||
// Compact "2h ago" (shared util) with the absolute time on hover.
|
||||
return (
|
||||
<span className="px-0.5 text-[0.6875rem] tabular-nums text-muted-foreground" title={absolute || undefined}>
|
||||
{relativeTime(date.getTime())}
|
||||
<span
|
||||
className="px-0.5 text-[0.6875rem] tabular-nums text-muted-foreground"
|
||||
title={formatMessageTimestamp(date, t.assistant.thread) || undefined}
|
||||
>
|
||||
{formatAgo(date.getTime(), t.agents)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1077,6 +1077,7 @@ export const en: Translations = {
|
|||
ageSeconds: seconds => `${seconds}s ago`,
|
||||
ageMinutes: minutes => `${minutes}m ago`,
|
||||
ageHours: hours => `${hours}h ago`,
|
||||
ageDays: days => `${days}d ago`,
|
||||
durationSeconds: seconds => `${seconds}s`,
|
||||
durationMinutes: (minutes, seconds) => `${minutes}m ${seconds}s`,
|
||||
tokens: value => `${value} tok`
|
||||
|
|
|
|||
|
|
@ -1057,6 +1057,7 @@ export const ja = defineLocale({
|
|||
ageSeconds: seconds => `${seconds}秒前`,
|
||||
ageMinutes: minutes => `${minutes}分前`,
|
||||
ageHours: hours => `${hours}時間前`,
|
||||
ageDays: days => `${days}日前`,
|
||||
durationSeconds: seconds => `${seconds}秒`,
|
||||
durationMinutes: (minutes, seconds) => `${minutes}分 ${seconds}秒`,
|
||||
tokens: value => `${value} トーク`
|
||||
|
|
|
|||
|
|
@ -949,6 +949,7 @@ export interface Translations {
|
|||
ageSeconds: (seconds: number) => string
|
||||
ageMinutes: (minutes: number) => string
|
||||
ageHours: (hours: number) => string
|
||||
ageDays: (days: number) => string
|
||||
durationSeconds: (seconds: string) => string
|
||||
durationMinutes: (minutes: number, seconds: number) => string
|
||||
tokens: (value: number | string) => string
|
||||
|
|
|
|||
|
|
@ -1025,6 +1025,7 @@ export const zhHant = defineLocale({
|
|||
ageSeconds: seconds => `${seconds} 秒前`,
|
||||
ageMinutes: minutes => `${minutes} 分鐘前`,
|
||||
ageHours: hours => `${hours} 小時前`,
|
||||
ageDays: days => `${days} 天前`,
|
||||
durationSeconds: seconds => `${seconds} 秒`,
|
||||
durationMinutes: (minutes, seconds) => `${minutes} 分 ${seconds} 秒`,
|
||||
tokens: value => `${value} 詞元`
|
||||
|
|
|
|||
|
|
@ -1272,6 +1272,7 @@ export const zh: Translations = {
|
|||
ageSeconds: seconds => `${seconds} 秒前`,
|
||||
ageMinutes: minutes => `${minutes} 分钟前`,
|
||||
ageHours: hours => `${hours} 小时前`,
|
||||
ageDays: days => `${days} 天前`,
|
||||
durationSeconds: seconds => `${seconds} 秒`,
|
||||
durationMinutes: (minutes, seconds) => `${minutes} 分 ${seconds} 秒`,
|
||||
tokens: value => `${value} 词元`
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ import {
|
|||
IconFolderOpen as FolderOpen,
|
||||
IconGitBranch as GitBranch,
|
||||
IconGitBranch as GitBranchIcon,
|
||||
IconGitFork as GitFork,
|
||||
IconGitFork as GitForkIcon,
|
||||
IconGlobe as Globe,
|
||||
IconHash as Hash,
|
||||
IconHelpCircle as HelpCircle,
|
||||
|
|
@ -163,6 +165,8 @@ export {
|
|||
FolderOpen,
|
||||
GitBranch,
|
||||
GitBranchIcon,
|
||||
GitFork,
|
||||
GitForkIcon,
|
||||
Globe,
|
||||
Hash,
|
||||
HelpCircle,
|
||||
|
|
|
|||
32
apps/desktop/src/lib/time.test.ts
Normal file
32
apps/desktop/src/lib/time.test.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { DAY, HOUR, MINUTE, SECOND, formatAgo } from './time'
|
||||
|
||||
const labels = {
|
||||
ageNow: 'now',
|
||||
ageSeconds: (s: number) => `${s}s ago`,
|
||||
ageMinutes: (m: number) => `${m}m ago`,
|
||||
ageHours: (h: number) => `${h}h ago`,
|
||||
ageDays: (d: number) => `${d}d ago`
|
||||
}
|
||||
|
||||
const now = 1_000 * DAY
|
||||
const ago = (delta: number) => formatAgo(now - delta, labels, now)
|
||||
|
||||
describe('formatAgo', () => {
|
||||
it('reads "now" under two seconds, then seconds', () => {
|
||||
expect(ago(0)).toBe('now')
|
||||
expect(ago(1.5 * SECOND)).toBe('now')
|
||||
expect(ago(5 * SECOND)).toBe('5s ago')
|
||||
})
|
||||
|
||||
it('buckets to the coarsest unit, floored', () => {
|
||||
expect(ago(3 * MINUTE)).toBe('3m ago')
|
||||
expect(ago(2 * HOUR + 59 * MINUTE)).toBe('2h ago')
|
||||
expect(ago(5 * DAY)).toBe('5d ago')
|
||||
})
|
||||
|
||||
it('clamps future timestamps to "now"', () => {
|
||||
expect(ago(-HOUR)).toBe('now')
|
||||
})
|
||||
})
|
||||
|
|
@ -72,3 +72,26 @@ export function coarseElapsed(deltaMs: number): { unit: ElapsedUnit; value: numb
|
|||
|
||||
return { unit: 'second', value: Math.floor(ms / SECOND) }
|
||||
}
|
||||
|
||||
// Localized strings for `formatAgo`; shaped to accept `t.agents` directly.
|
||||
export interface AgoLabels {
|
||||
ageNow: string
|
||||
ageSeconds: (seconds: number) => string
|
||||
ageMinutes: (minutes: number) => string
|
||||
ageHours: (hours: number) => string
|
||||
ageDays: (days: number) => string
|
||||
}
|
||||
|
||||
// Compact localized "2h ago" / "3m ago" / "now" for a past timestamp, bucketed
|
||||
// via `coarseElapsed` so every age label reads consistently.
|
||||
export function formatAgo(fromMs: number, labels: AgoLabels, nowMs = Date.now()): string {
|
||||
const { unit, value } = coarseElapsed(nowMs - fromMs)
|
||||
|
||||
if (unit === 'second') {
|
||||
return value < 2 ? labels.ageNow : labels.ageSeconds(value)
|
||||
}
|
||||
|
||||
const by = { day: labels.ageDays, hour: labels.ageHours, minute: labels.ageMinutes }
|
||||
|
||||
return by[unit](value)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue