mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat: improve settings overlay and tool message display
- Refactor HappyComposer settings overlay to conditionally render permission and model settings based on their availability - Add showPermissionSettings and showModelSettings flags for cleaner conditional rendering - Enhance ToolMessage fallback UI with styled card layout and code blocks for arguments and results - Improve type validation in isToolCallBlock with comprehensive property checks - Add safeStringify utility for robust JSON serialization with fallback handling
This commit is contained in:
@@ -269,85 +269,93 @@ export function HappyComposer(props: {
|
||||
haptic('light')
|
||||
}, [onModelModeChange, controlsDisabled, haptic])
|
||||
|
||||
const showPermissionSettings = Boolean(onPermissionModeChange)
|
||||
const showModelSettings = Boolean(onModelModeChange)
|
||||
const showSettingsButton = Boolean(onPermissionModeChange || onModelModeChange)
|
||||
const showAbortButton = true
|
||||
|
||||
const overlays = useMemo(() => {
|
||||
if (showSettings) {
|
||||
if (showSettings && (showPermissionSettings || showModelSettings)) {
|
||||
return (
|
||||
<div className="absolute bottom-[100%] mb-2 w-full">
|
||||
<FloatingOverlay maxHeight={320}>
|
||||
<div className="py-2">
|
||||
<div className="px-3 pb-1 text-xs font-semibold text-[var(--app-hint)]">
|
||||
Permission Mode
|
||||
</div>
|
||||
{PERMISSION_MODES.map((mode) => (
|
||||
<button
|
||||
key={mode}
|
||||
type="button"
|
||||
disabled={controlsDisabled}
|
||||
className={`flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors ${
|
||||
controlsDisabled
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: 'cursor-pointer hover:bg-[var(--app-secondary-bg)]'
|
||||
}`}
|
||||
onClick={() => handlePermissionChange(mode)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<div
|
||||
className={`flex h-4 w-4 items-center justify-center rounded-full border-2 ${
|
||||
permissionMode === mode
|
||||
? 'border-[var(--app-link)]'
|
||||
: 'border-[var(--app-hint)]'
|
||||
{showPermissionSettings ? (
|
||||
<div className="py-2">
|
||||
<div className="px-3 pb-1 text-xs font-semibold text-[var(--app-hint)]">
|
||||
Permission Mode
|
||||
</div>
|
||||
{PERMISSION_MODES.map((mode) => (
|
||||
<button
|
||||
key={mode}
|
||||
type="button"
|
||||
disabled={controlsDisabled}
|
||||
className={`flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors ${
|
||||
controlsDisabled
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: 'cursor-pointer hover:bg-[var(--app-secondary-bg)]'
|
||||
}`}
|
||||
onClick={() => handlePermissionChange(mode)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
>
|
||||
{permissionMode === mode && (
|
||||
<div className="h-2 w-2 rounded-full bg-[var(--app-link)]" />
|
||||
)}
|
||||
</div>
|
||||
<span className={permissionMode === mode ? 'text-[var(--app-link)]' : ''}>
|
||||
{PERMISSION_MODE_LABELS[mode]}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mx-3 h-px bg-[var(--app-divider)]" />
|
||||
|
||||
<div className="py-2">
|
||||
<div className="px-3 pb-1 text-xs font-semibold text-[var(--app-hint)]">
|
||||
Model
|
||||
<div
|
||||
className={`flex h-4 w-4 items-center justify-center rounded-full border-2 ${
|
||||
permissionMode === mode
|
||||
? 'border-[var(--app-link)]'
|
||||
: 'border-[var(--app-hint)]'
|
||||
}`}
|
||||
>
|
||||
{permissionMode === mode && (
|
||||
<div className="h-2 w-2 rounded-full bg-[var(--app-link)]" />
|
||||
)}
|
||||
</div>
|
||||
<span className={permissionMode === mode ? 'text-[var(--app-link)]' : ''}>
|
||||
{PERMISSION_MODE_LABELS[mode]}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{MODEL_MODES.map((mode) => (
|
||||
<button
|
||||
key={mode}
|
||||
type="button"
|
||||
disabled={controlsDisabled}
|
||||
className={`flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors ${
|
||||
controlsDisabled
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: 'cursor-pointer hover:bg-[var(--app-secondary-bg)]'
|
||||
}`}
|
||||
onClick={() => handleModelChange(mode)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<div
|
||||
className={`flex h-4 w-4 items-center justify-center rounded-full border-2 ${
|
||||
modelMode === mode
|
||||
? 'border-[var(--app-link)]'
|
||||
: 'border-[var(--app-hint)]'
|
||||
) : null}
|
||||
|
||||
{showPermissionSettings && showModelSettings ? (
|
||||
<div className="mx-3 h-px bg-[var(--app-divider)]" />
|
||||
) : null}
|
||||
|
||||
{showModelSettings ? (
|
||||
<div className="py-2">
|
||||
<div className="px-3 pb-1 text-xs font-semibold text-[var(--app-hint)]">
|
||||
Model
|
||||
</div>
|
||||
{MODEL_MODES.map((mode) => (
|
||||
<button
|
||||
key={mode}
|
||||
type="button"
|
||||
disabled={controlsDisabled}
|
||||
className={`flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors ${
|
||||
controlsDisabled
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: 'cursor-pointer hover:bg-[var(--app-secondary-bg)]'
|
||||
}`}
|
||||
onClick={() => handleModelChange(mode)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
>
|
||||
{modelMode === mode && (
|
||||
<div className="h-2 w-2 rounded-full bg-[var(--app-link)]" />
|
||||
)}
|
||||
</div>
|
||||
<span className={modelMode === mode ? 'text-[var(--app-link)]' : ''}>
|
||||
{MODEL_MODE_LABELS[mode]}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className={`flex h-4 w-4 items-center justify-center rounded-full border-2 ${
|
||||
modelMode === mode
|
||||
? 'border-[var(--app-link)]'
|
||||
: 'border-[var(--app-hint)]'
|
||||
}`}
|
||||
>
|
||||
{modelMode === mode && (
|
||||
<div className="h-2 w-2 rounded-full bg-[var(--app-link)]" />
|
||||
)}
|
||||
</div>
|
||||
<span className={modelMode === mode ? 'text-[var(--app-link)]' : ''}>
|
||||
{MODEL_MODE_LABELS[mode]}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</FloatingOverlay>
|
||||
</div>
|
||||
)
|
||||
@@ -370,6 +378,8 @@ export function HappyComposer(props: {
|
||||
return null
|
||||
}, [
|
||||
showSettings,
|
||||
showPermissionSettings,
|
||||
showModelSettings,
|
||||
suggestions,
|
||||
selectedIndex,
|
||||
controlsDisabled,
|
||||
|
||||
@@ -2,6 +2,7 @@ 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 { CodeBlock } from '@/components/CodeBlock'
|
||||
import { MarkdownRenderer } from '@/components/MarkdownRenderer'
|
||||
import { LazyRainbowText } from '@/components/LazyRainbowText'
|
||||
import { ToolCard } from '@/components/ToolCard/ToolCard'
|
||||
@@ -11,11 +12,28 @@ function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === 'object'
|
||||
}
|
||||
|
||||
function safeStringify(value: unknown): string {
|
||||
if (typeof value === 'string') return value
|
||||
try {
|
||||
const stringified = JSON.stringify(value, null, 2)
|
||||
return typeof stringified === 'string' ? stringified : String(value)
|
||||
} catch {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
|
||||
function isToolCallBlock(value: unknown): value is ToolCallBlock {
|
||||
if (!isObject(value)) return false
|
||||
if (value.kind !== 'tool-call') return false
|
||||
if (typeof value.id !== 'string') return false
|
||||
if (value.localId !== null && typeof value.localId !== 'string') return false
|
||||
if (typeof value.createdAt !== 'number') return false
|
||||
if (!Array.isArray(value.children)) return false
|
||||
if (!isObject(value.tool)) return false
|
||||
if (typeof value.tool.name !== 'string') return false
|
||||
if (!('input' in value.tool)) return false
|
||||
if (value.tool.description !== null && typeof value.tool.description !== 'string') return false
|
||||
if (value.tool.state !== 'pending' && value.tool.state !== 'running' && value.tool.state !== 'completed' && value.tool.state !== 'error') return false
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -182,10 +200,37 @@ export function HappyToolMessage(props: ToolCallMessagePartProps) {
|
||||
const artifact = props.artifact
|
||||
|
||||
if (!isToolCallBlock(artifact)) {
|
||||
const argsText = typeof props.argsText === 'string' ? props.argsText.trim() : ''
|
||||
const hasArgsText = argsText.length > 0
|
||||
const hasResult = props.result !== undefined
|
||||
const resultText = hasResult ? safeStringify(props.result) : ''
|
||||
|
||||
return (
|
||||
<div className="py-1">
|
||||
<div className="text-xs text-[var(--app-hint)]">
|
||||
Tool call: {props.toolName}
|
||||
<div className="rounded-xl bg-[var(--app-secondary-bg)] p-3 shadow-sm">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<div className="font-mono text-[var(--app-hint)]">
|
||||
Tool: {props.toolName}
|
||||
</div>
|
||||
{props.isError ? (
|
||||
<span className="text-red-500">Error</span>
|
||||
) : null}
|
||||
{props.status.type === 'running' && !hasResult ? (
|
||||
<span className="text-[var(--app-hint)]">Running…</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{hasArgsText ? (
|
||||
<div className="mt-2">
|
||||
<CodeBlock code={argsText} language="json" />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{hasResult ? (
|
||||
<div className="mt-2">
|
||||
<CodeBlock code={resultText} language={typeof props.result === 'string' ? 'text' : 'json'} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user