refactor: extract presentation utilities and status indicator component

This commit is contained in:
weishu
2025-12-18 08:47:13 +08:00
parent 120de5515d
commit 7f07bdc796
5 changed files with 80 additions and 146 deletions
+37
View File
@@ -0,0 +1,37 @@
import type { AgentEvent } from '@/chat/types'
export function formatUnixTimestamp(value: number): string {
const ms = value < 1_000_000_000_000 ? value * 1000 : value
const date = new Date(ms)
if (Number.isNaN(date.getTime())) return String(value)
return date.toLocaleString()
}
export function renderEventLabel(event: AgentEvent): string {
if (event.type === 'switch') {
const mode = event.mode === 'local' ? 'local' : 'remote'
return `🔄 Switched to ${mode}`
}
if (event.type === 'title-changed') {
const title = typeof event.title === 'string' ? event.title : ''
return title ? `Title changed to "${title}"` : 'Title changed'
}
if (event.type === 'permission-mode-changed') {
const modeValue = (event as Record<string, unknown>).mode
const mode = typeof modeValue === 'string' ? modeValue : 'default'
return `🔐 Permission mode: ${mode}`
}
if (event.type === 'limit-reached') {
const endsAt = typeof event.endsAt === 'number' ? event.endsAt : null
return endsAt ? `⏳ Usage limit reached until ${formatUnixTimestamp(endsAt)}` : '⏳ Usage limit reached'
}
if (event.type === 'message') {
return typeof event.message === 'string' ? event.message : 'Message'
}
try {
return JSON.stringify(event)
} catch {
return String(event.type)
}
}
@@ -0,0 +1,38 @@
import type { MessageStatus } from '@/types/api'
function ErrorIcon() {
return (
<svg className="h-[14px] w-[14px]" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.5" />
<path d="M8 5v4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
<circle cx="8" cy="11" r="0.75" fill="currentColor" />
</svg>
)
}
export function MessageStatusIndicator(props: {
status?: MessageStatus
onRetry?: () => void
}) {
if (props.status !== 'failed') {
return null
}
return (
<span className="inline-flex items-center gap-1">
<span className="text-red-500">
<ErrorIcon />
</span>
{props.onRetry ? (
<button
type="button"
onClick={props.onRetry}
className="text-xs text-blue-500 hover:underline"
>
</button>
) : null}
</span>
)
}
@@ -1,10 +1,11 @@
import type { ToolCallMessagePartProps } from '@assistant-ui/react'
import type { ChatBlock } from '@/chat/types'
import type { AgentEvent, ToolCallBlock } from '@/chat/types'
import type { MessageStatus } from '@/types/api'
import type { ToolCallBlock } from '@/chat/types'
import { renderEventLabel } from '@/chat/presentation'
import { CodeBlock } from '@/components/CodeBlock'
import { MarkdownRenderer } from '@/components/MarkdownRenderer'
import { LazyRainbowText } from '@/components/LazyRainbowText'
import { MessageStatusIndicator } from '@/components/AssistantChat/messages/MessageStatusIndicator'
import { ToolCard } from '@/components/ToolCard/ToolCard'
import { useHappyChatContext } from '@/components/AssistantChat/context'
@@ -37,77 +38,6 @@ function isToolCallBlock(value: unknown): value is ToolCallBlock {
return true
}
function ErrorIcon() {
return (
<svg className="h-[14px] w-[14px]" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.5" />
<path d="M8 5v4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
<circle cx="8" cy="11" r="0.75" fill="currentColor" />
</svg>
)
}
function MessageStatusIndicator(props: {
status?: MessageStatus
onRetry?: () => void
}) {
if (props.status !== 'failed') {
return null
}
return (
<span className="inline-flex items-center gap-1">
<span className="text-red-500">
<ErrorIcon />
</span>
{props.onRetry ? (
<button
type="button"
onClick={props.onRetry}
className="text-xs text-blue-500 hover:underline"
>
</button>
) : null}
</span>
)
}
function formatUnixTimestamp(value: number): string {
const ms = value < 1_000_000_000_000 ? value * 1000 : value
const date = new Date(ms)
if (Number.isNaN(date.getTime())) return String(value)
return date.toLocaleString()
}
function renderEventLabel(event: AgentEvent): string {
if (event.type === 'switch') {
const mode = event.mode === 'local' ? 'local' : 'remote'
return `🔄 Switched to ${mode}`
}
if (event.type === 'title-changed') {
const title = typeof event.title === 'string' ? event.title : ''
return title ? `Title changed to "${title}"` : 'Title changed'
}
if (event.type === 'permission-mode-changed') {
const modeValue = (event as Record<string, unknown>).mode
const mode = typeof modeValue === 'string' ? modeValue : 'default'
return `🔐 Permission mode: ${mode}`
}
if (event.type === 'limit-reached') {
const endsAt = typeof event.endsAt === 'number' ? event.endsAt : null
return endsAt ? `⏳ Usage limit reached until ${formatUnixTimestamp(endsAt)}` : '⏳ Usage limit reached'
}
if (event.type === 'message') {
return typeof event.message === 'string' ? event.message : 'Message'
}
try {
return JSON.stringify(event)
} catch {
return String(event.type)
}
}
function HappyNestedBlockList(props: {
blocks: ChatBlock[]
}) {
@@ -1,44 +1,8 @@
import { MessagePrimitive, useAssistantState } from '@assistant-ui/react'
import type { MessageStatus } from '@/types/api'
import { LazyRainbowText } from '@/components/LazyRainbowText'
import { useHappyChatContext } from '@/components/AssistantChat/context'
import type { HappyChatMessageMetadata } from '@/lib/assistant-runtime'
function ErrorIcon() {
return (
<svg className="h-[14px] w-[14px]" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.5" />
<path d="M8 5v4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
<circle cx="8" cy="11" r="0.75" fill="currentColor" />
</svg>
)
}
function MessageStatusIndicator(props: {
status?: MessageStatus
onRetry?: () => void
}) {
if (props.status !== 'failed') {
return null
}
return (
<span className="inline-flex items-center gap-1">
<span className="text-red-500">
<ErrorIcon />
</span>
{props.onRetry ? (
<button
type="button"
onClick={props.onRetry}
className="text-xs text-blue-500 hover:underline"
>
</button>
) : null}
</span>
)
}
import { MessageStatusIndicator } from '@/components/AssistantChat/messages/MessageStatusIndicator'
export function HappyUserMessage() {
const ctx = useHappyChatContext()
@@ -70,4 +34,3 @@ export function HappyUserMessage() {
</MessagePrimitive.Root>
)
}
+1 -35
View File
@@ -1,6 +1,7 @@
import { useMemo } from 'react'
import type { AppendMessage, ExternalStoreAdapter, ThreadMessageLike } from '@assistant-ui/react'
import { useExternalStoreRuntime } from '@assistant-ui/react'
import { renderEventLabel } from '@/chat/presentation'
import type { ChatBlock } from '@/chat/types'
import type { AgentEvent, ToolCallBlock } from '@/chat/types'
import type { MessageStatus as HappyMessageStatus, Session } from '@/types/api'
@@ -15,41 +16,6 @@ function safeStringify(value: unknown): string {
}
}
function formatUnixTimestamp(value: number): string {
const ms = value < 1_000_000_000_000 ? value * 1000 : value
const date = new Date(ms)
if (Number.isNaN(date.getTime())) return String(value)
return date.toLocaleString()
}
function renderEventLabel(event: AgentEvent): string {
if (event.type === 'switch') {
const mode = event.mode === 'local' ? 'local' : 'remote'
return `🔄 Switched to ${mode}`
}
if (event.type === 'title-changed') {
const title = typeof event.title === 'string' ? event.title : ''
return title ? `Title changed to "${title}"` : 'Title changed'
}
if (event.type === 'permission-mode-changed') {
const modeValue = (event as Record<string, unknown>).mode
const mode = typeof modeValue === 'string' ? modeValue : 'default'
return `🔐 Permission mode: ${mode}`
}
if (event.type === 'limit-reached') {
const endsAt = typeof event.endsAt === 'number' ? event.endsAt : null
return endsAt ? `⏳ Usage limit reached until ${formatUnixTimestamp(endsAt)}` : '⏳ Usage limit reached'
}
if (event.type === 'message') {
return typeof event.message === 'string' ? event.message : 'Message'
}
try {
return JSON.stringify(event)
} catch {
return String(event.type)
}
}
export type HappyChatMessageMetadata = {
kind: 'user' | 'assistant' | 'tool' | 'event'
status?: HappyMessageStatus