mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(web): open tool details from inline previews (#725)
* fix(web): open tool details from inline previews * fix(web): ignore nested controls in tool previews
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 325 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 419 KiB |
@@ -78,7 +78,10 @@ export function CodeBlock(props: {
|
|||||||
{showCopyButton ? (
|
{showCopyButton ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => copy(props.code)}
|
onClick={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
copy(props.code)
|
||||||
|
}}
|
||||||
className="shrink-0 rounded-md p-1 text-[var(--app-code-header-fg)] transition-colors hover:bg-[var(--app-code-copy-hover-bg)] hover:text-[var(--app-fg)]"
|
className="shrink-0 rounded-md p-1 text-[var(--app-code-header-fg)] transition-colors hover:bg-[var(--app-code-copy-hover-bg)] hover:text-[var(--app-fg)]"
|
||||||
title={t('code.copy')}
|
title={t('code.copy')}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ToolCallBlock } from '@/chat/types'
|
import type { ToolCallBlock } from '@/chat/types'
|
||||||
import type { ApiClient } from '@/api/client'
|
import type { ApiClient } from '@/api/client'
|
||||||
import type { SessionMetadataSummary } from '@/types/api'
|
import type { SessionMetadataSummary } from '@/types/api'
|
||||||
import { memo, useEffect, useMemo, useState, type ReactNode } from 'react'
|
import { memo, useEffect, useMemo, useState, type KeyboardEvent, type MouseEvent, type ReactNode } from 'react'
|
||||||
import { isObject, safeStringify } from '@hapi/protocol'
|
import { isObject, safeStringify } from '@hapi/protocol'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { CodeBlock } from '@/components/CodeBlock'
|
import { CodeBlock } from '@/components/CodeBlock'
|
||||||
@@ -187,6 +187,16 @@ function DetailsIcon() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const INLINE_PREVIEW_INTERACTIVE_SELECTOR = 'a, button, input, textarea, select, summary, [role="button"], [contenteditable="true"]'
|
||||||
|
|
||||||
|
function isNestedInteractiveElement(event: MouseEvent<HTMLElement> | KeyboardEvent<HTMLElement>): boolean {
|
||||||
|
if (event.target === event.currentTarget) return false
|
||||||
|
if (!(event.target instanceof Element)) return false
|
||||||
|
|
||||||
|
const interactive = event.target.closest(INLINE_PREVIEW_INTERACTIVE_SELECTOR)
|
||||||
|
return interactive !== null && interactive !== event.currentTarget
|
||||||
|
}
|
||||||
|
|
||||||
type ToolCardProps = {
|
type ToolCardProps = {
|
||||||
api: ApiClient
|
api: ApiClient
|
||||||
sessionId: string
|
sessionId: string
|
||||||
@@ -238,6 +248,7 @@ export function ToolDetailDialogContent(props: {
|
|||||||
|
|
||||||
function ToolCardInner(props: ToolCardProps) {
|
function ToolCardInner(props: ToolCardProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const [detailsOpen, setDetailsOpen] = useState(false)
|
||||||
const presentation = useMemo(() => getToolPresentation({
|
const presentation = useMemo(() => getToolPresentation({
|
||||||
toolName: props.block.tool.name,
|
toolName: props.block.tool.name,
|
||||||
input: props.block.tool.input,
|
input: props.block.tool.input,
|
||||||
@@ -276,6 +287,18 @@ function ToolCardInner(props: ToolCardProps) {
|
|||||||
const hasBody = showInline || taskSummary !== null || showsPermissionFooter
|
const hasBody = showInline || taskSummary !== null || showsPermissionFooter
|
||||||
const stateColor = toolStatusColorClass(props.block.tool.state)
|
const stateColor = toolStatusColorClass(props.block.tool.state)
|
||||||
const { suppressFocusRing, onTriggerPointerDown, onTriggerKeyDown, onTriggerBlur } = usePointerFocusRing()
|
const { suppressFocusRing, onTriggerPointerDown, onTriggerKeyDown, onTriggerBlur } = usePointerFocusRing()
|
||||||
|
const openDetails = () => setDetailsOpen(true)
|
||||||
|
const openDetailsFromInlinePreview = (event: MouseEvent<HTMLElement>) => {
|
||||||
|
if (isNestedInteractiveElement(event)) return
|
||||||
|
openDetails()
|
||||||
|
}
|
||||||
|
const openDetailsFromInlinePreviewKeyDown = (event: KeyboardEvent<HTMLElement>) => {
|
||||||
|
if (isNestedInteractiveElement(event)) return
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault()
|
||||||
|
openDetails()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const header = (
|
const header = (
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
@@ -320,7 +343,7 @@ function ToolCardInner(props: ToolCardProps) {
|
|||||||
return (
|
return (
|
||||||
<Card className="overflow-hidden rounded-[20px] bg-[var(--app-tool-card-bg)] shadow-none">
|
<Card className="overflow-hidden rounded-[20px] bg-[var(--app-tool-card-bg)] shadow-none">
|
||||||
<CardHeader className={cn('space-y-0 p-3', subtitle ? 'pb-2' : null)}>
|
<CardHeader className={cn('space-y-0 p-3', subtitle ? 'pb-2' : null)}>
|
||||||
<Dialog>
|
<Dialog open={detailsOpen} onOpenChange={setDetailsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -354,16 +377,34 @@ function ToolCardInner(props: ToolCardProps) {
|
|||||||
|
|
||||||
{showInline ? (
|
{showInline ? (
|
||||||
CompactToolView ? (
|
CompactToolView ? (
|
||||||
<div className="mt-3">
|
<div
|
||||||
|
className="mt-3 cursor-pointer rounded-xl focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--app-link)]"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={openDetailsFromInlinePreview}
|
||||||
|
onKeyDown={openDetailsFromInlinePreviewKeyDown}
|
||||||
|
>
|
||||||
<CompactToolView block={props.block} metadata={props.metadata} surface="inline" />
|
<CompactToolView block={props.block} metadata={props.metadata} surface="inline" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-3 flex flex-col gap-3">
|
<div className="mt-3 flex flex-col gap-3">
|
||||||
<div>
|
<div
|
||||||
|
className="cursor-pointer rounded-xl focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--app-link)]"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={openDetailsFromInlinePreview}
|
||||||
|
onKeyDown={openDetailsFromInlinePreviewKeyDown}
|
||||||
|
>
|
||||||
<div className="mb-1 text-xs font-medium text-[var(--app-hint)]">{t('tool.input')}</div>
|
<div className="mb-1 text-xs font-medium text-[var(--app-hint)]">{t('tool.input')}</div>
|
||||||
{renderToolInput(props.block, 'inline')}
|
{renderToolInput(props.block, 'inline')}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div
|
||||||
|
className="cursor-pointer rounded-xl focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--app-link)]"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={openDetailsFromInlinePreview}
|
||||||
|
onKeyDown={openDetailsFromInlinePreviewKeyDown}
|
||||||
|
>
|
||||||
<div className="mb-1 text-xs font-medium text-[var(--app-hint)]">{t('tool.result')}</div>
|
<div className="mb-1 text-xs font-medium text-[var(--app-hint)]">{t('tool.result')}</div>
|
||||||
<ResultToolView block={props.block} metadata={props.metadata} surface="inline" />
|
<ResultToolView block={props.block} metadata={props.metadata} surface="inline" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user