mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-07-10 00:14:27 +00:00
Merge branch 'dev' of github.com:NeuralNomadsAI/CodeNomad into dev
This commit is contained in:
commit
d3950df816
14 changed files with 495 additions and 29 deletions
85
packages/ui/src/components/action-overflow-menu.tsx
Normal file
85
packages/ui/src/components/action-overflow-menu.tsx
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import { DropdownMenu } from "@kobalte/core/dropdown-menu"
|
||||
import { For, Show, createSignal, onCleanup, type JSXElement } from "solid-js"
|
||||
import { MoreHorizontal } from "lucide-solid"
|
||||
|
||||
export interface ActionOverflowMenuItem {
|
||||
key: string
|
||||
label: string
|
||||
icon?: JSXElement
|
||||
disabled?: boolean
|
||||
destructive?: boolean
|
||||
onSelect: () => void | Promise<void>
|
||||
onMouseEnter?: () => void
|
||||
onMouseLeave?: () => void
|
||||
}
|
||||
|
||||
interface ActionOverflowMenuProps {
|
||||
items: ActionOverflowMenuItem[]
|
||||
label: string
|
||||
triggerClass?: string
|
||||
minItems?: number
|
||||
}
|
||||
|
||||
export default function ActionOverflowMenu(props: ActionOverflowMenuProps) {
|
||||
const [hoveredItem, setHoveredItem] = createSignal<ActionOverflowMenuItem | null>(null)
|
||||
const enabledItems = () => props.items.filter((item) => !item.disabled)
|
||||
const hasItems = () => props.items.length >= (props.minItems ?? 1)
|
||||
const clearHoveredItem = () => {
|
||||
const item = hoveredItem()
|
||||
if (!item) return
|
||||
item.onMouseLeave?.()
|
||||
setHoveredItem(null)
|
||||
}
|
||||
|
||||
onCleanup(clearHoveredItem)
|
||||
|
||||
return (
|
||||
<Show when={hasItems()}>
|
||||
<DropdownMenu placement="bottom-end" gutter={4} onOpenChange={(open) => { if (!open) clearHoveredItem() }}>
|
||||
<DropdownMenu.Trigger
|
||||
class={`action-overflow-trigger ${props.triggerClass ?? ""}`.trim()}
|
||||
aria-label={props.label}
|
||||
title={props.label}
|
||||
disabled={enabledItems().length === 0}
|
||||
>
|
||||
<MoreHorizontal class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content class="action-overflow-content">
|
||||
<For each={props.items}>
|
||||
{(item) => (
|
||||
<DropdownMenu.Item
|
||||
class="action-overflow-item"
|
||||
data-destructive={item.destructive ? "true" : undefined}
|
||||
disabled={item.disabled}
|
||||
onPointerEnter={() => {
|
||||
if (item.disabled) return
|
||||
const previous = hoveredItem()
|
||||
if (previous !== item) previous?.onMouseLeave?.()
|
||||
setHoveredItem(item)
|
||||
item.onMouseEnter?.()
|
||||
}}
|
||||
onPointerLeave={() => {
|
||||
if (item.disabled) return
|
||||
if (hoveredItem() === item) setHoveredItem(null)
|
||||
item.onMouseLeave?.()
|
||||
}}
|
||||
onSelect={() => {
|
||||
clearHoveredItem()
|
||||
void item.onSelect()
|
||||
}}
|
||||
>
|
||||
<Show when={item.icon} fallback={<span class="action-overflow-item-icon" aria-hidden="true" />}>
|
||||
{(icon) => <span class="action-overflow-item-icon" aria-hidden="true">{icon()}</span>}
|
||||
</Show>
|
||||
<span class="action-overflow-item-label">{item.label}</span>
|
||||
</DropdownMenu.Item>
|
||||
)}
|
||||
</For>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { For, Index, Match, Show, Suspense, Switch, createEffect, createMemo, createSignal, lazy, onCleanup, untrack, type Accessor } from "solid-js"
|
||||
import { ChevronsDownUp, ChevronsUpDown, ExternalLink, FoldVertical, ListStart, Trash } from "lucide-solid"
|
||||
import { ChevronsDownUp, ChevronsUpDown, ExternalLink, FoldVertical, ListStart, Trash, Volume2 } from "lucide-solid"
|
||||
import MessageItem from "./message-item"
|
||||
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
||||
import type { ClientPart, MessageInfo } from "../types/message"
|
||||
|
|
@ -18,6 +18,7 @@ import { useSpeech } from "../lib/hooks/use-speech"
|
|||
import SpeechActionButton from "./speech-action-button"
|
||||
import { createFollowScroll } from "../lib/follow-scroll"
|
||||
import type { SessionSearchMatch } from "../lib/session-search"
|
||||
import ActionOverflowMenu, { type ActionOverflowMenuItem } from "./action-overflow-menu"
|
||||
|
||||
function DeleteUpToIcon() {
|
||||
return (
|
||||
|
|
@ -486,6 +487,12 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||
navigateToTaskSession(location)
|
||||
}
|
||||
|
||||
const goToTaskSession = () => {
|
||||
const location = taskLocation()
|
||||
if (!location) return
|
||||
navigateToTaskSession(location)
|
||||
}
|
||||
|
||||
const handleDeleteMessage = async (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
|
@ -507,9 +514,7 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||
}
|
||||
}
|
||||
|
||||
const handleDeleteUpTo = async (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const deleteUpTo = async () => {
|
||||
if (!props.showDeleteMessage) return
|
||||
if (!props.onDeleteMessagesUpTo) return
|
||||
if (deletingUpTo()) return
|
||||
|
|
@ -522,11 +527,72 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||
}
|
||||
}
|
||||
|
||||
const handleDeleteUpTo = async (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
await deleteUpTo()
|
||||
}
|
||||
|
||||
const actionMenuItems = (): ActionOverflowMenuItem[] => {
|
||||
const items: ActionOverflowMenuItem[] = []
|
||||
|
||||
if (taskSessionId()) {
|
||||
items.push({
|
||||
key: "go-to-session",
|
||||
label: t("messageBlock.tool.goToSession.label"),
|
||||
icon: <ExternalLink class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
disabled: !taskLocation(),
|
||||
onSelect: goToTaskSession,
|
||||
})
|
||||
}
|
||||
|
||||
if (props.showDeleteMessage) {
|
||||
items.push(
|
||||
{
|
||||
key: "delete-up-to",
|
||||
label: t("messageItem.actions.deleteMessagesUpTo"),
|
||||
icon: <DeleteUpToIcon />,
|
||||
disabled: !props.onDeleteMessagesUpTo || deletingUpTo(),
|
||||
destructive: true,
|
||||
onMouseEnter: () => props.onDeleteHoverChange?.({ kind: "deleteUpTo", messageId: props.messageId }),
|
||||
onMouseLeave: () => props.onDeleteHoverChange?.({ kind: "none" }),
|
||||
onSelect: deleteUpTo,
|
||||
},
|
||||
{
|
||||
key: "delete-message",
|
||||
label: deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage"),
|
||||
icon: <Trash class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
disabled: deletingMessage(),
|
||||
destructive: true,
|
||||
onMouseEnter: () => props.onDeleteHoverChange?.({ kind: "message", messageId: props.messageId }),
|
||||
onMouseLeave: () => props.onDeleteHoverChange?.({ kind: "none" }),
|
||||
onSelect: async () => {
|
||||
if (deletingMessage()) return
|
||||
setDeletingMessage(true)
|
||||
try {
|
||||
await deleteMessage(props.instanceId, props.sessionId, props.messageId)
|
||||
} catch (error) {
|
||||
showAlertDialog(t("messageItem.actions.deleteMessageFailedMessage"), {
|
||||
title: t("messageItem.actions.deleteMessageFailedTitle"),
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
variant: "error",
|
||||
})
|
||||
} finally {
|
||||
setDeletingMessage(false)
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
return (
|
||||
<Show when={toolPart()}>
|
||||
{(resolvedToolPart) => (
|
||||
<div class="delete-hover-scope" data-delete-part-hover={isDeleteOverlayActive() ? "true" : undefined}>
|
||||
<div class="tool-call-header-label">
|
||||
<div class="tool-call-header-label" data-action-overflow={actionMenuItems().length > 1 ? "true" : undefined}>
|
||||
<div class="tool-call-header-meta">
|
||||
<Show when={props.showDeleteMessage}>
|
||||
<input
|
||||
|
|
@ -551,7 +617,7 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||
<span class="tool-name">{toolName() || t("messageBlock.tool.unknown")}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-0">
|
||||
<div class="tool-call-header-actions flex items-center gap-0">
|
||||
<Show when={taskSessionId()}>
|
||||
<button
|
||||
class="tool-call-header-button"
|
||||
|
|
@ -593,6 +659,12 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
<ActionOverflowMenu
|
||||
items={actionMenuItems()}
|
||||
label={t("messageItem.actions.more")}
|
||||
triggerClass="tool-call-header-button"
|
||||
minItems={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Suspense fallback={<ToolCallFallback />}>
|
||||
|
|
@ -1582,6 +1654,73 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||
|
||||
const canDeleteMessage = () => Boolean(props.showDeleteMessage) && !deletingMessage()
|
||||
|
||||
const deleteUpTo = async () => {
|
||||
if (!props.showDeleteMessage) return
|
||||
if (!props.onDeleteMessagesUpTo) return
|
||||
if (deletingUpTo()) return
|
||||
|
||||
setDeletingUpTo(true)
|
||||
try {
|
||||
await props.onDeleteMessagesUpTo(props.messageId)
|
||||
} finally {
|
||||
setDeletingUpTo(false)
|
||||
}
|
||||
}
|
||||
|
||||
const actionMenuItems = (): ActionOverflowMenuItem[] => {
|
||||
const items: ActionOverflowMenuItem[] = []
|
||||
|
||||
if (canSpeakReasoning()) {
|
||||
items.push({
|
||||
key: "speak",
|
||||
label: speech.buttonTitle(),
|
||||
icon: <Volume2 class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
onSelect: () => void speech.toggle(),
|
||||
})
|
||||
}
|
||||
|
||||
if (props.showDeleteMessage) {
|
||||
items.push(
|
||||
{
|
||||
key: "delete-up-to",
|
||||
label: t("messageItem.actions.deleteMessagesUpTo"),
|
||||
icon: <DeleteUpToIcon />,
|
||||
disabled: !props.onDeleteMessagesUpTo || deletingUpTo(),
|
||||
destructive: true,
|
||||
onMouseEnter: () => props.onDeleteHoverChange?.({ kind: "deleteUpTo", messageId: props.messageId }),
|
||||
onMouseLeave: () => props.onDeleteHoverChange?.({ kind: "none" }),
|
||||
onSelect: deleteUpTo,
|
||||
},
|
||||
{
|
||||
key: "delete-message",
|
||||
label: deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage"),
|
||||
icon: <Trash class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
disabled: !canDeleteMessage(),
|
||||
destructive: true,
|
||||
onMouseEnter: () => props.onDeleteHoverChange?.({ kind: "message", messageId: props.messageId }),
|
||||
onMouseLeave: () => props.onDeleteHoverChange?.({ kind: "none" }),
|
||||
onSelect: async () => {
|
||||
if (!canDeleteMessage()) return
|
||||
setDeletingMessage(true)
|
||||
try {
|
||||
await deleteMessage(props.instanceId, props.sessionId, props.messageId)
|
||||
} catch (error) {
|
||||
showAlertDialog(t("messageItem.actions.deleteMessageFailedMessage"), {
|
||||
title: t("messageItem.actions.deleteMessageFailedTitle"),
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
variant: "error",
|
||||
})
|
||||
} finally {
|
||||
setDeletingMessage(false)
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
const handleDeleteMessage = async (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
|
@ -1604,16 +1743,7 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||
const handleDeleteUpTo = async (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (!props.showDeleteMessage) return
|
||||
if (!props.onDeleteMessagesUpTo) return
|
||||
if (deletingUpTo()) return
|
||||
|
||||
setDeletingUpTo(true)
|
||||
try {
|
||||
await props.onDeleteMessagesUpTo(props.messageId)
|
||||
} finally {
|
||||
setDeletingUpTo(false)
|
||||
}
|
||||
await deleteUpTo()
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -1654,7 +1784,7 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||
</span>
|
||||
</button>
|
||||
|
||||
<div class="message-reasoning-actions">
|
||||
<div class="message-reasoning-actions" data-action-overflow={actionMenuItems().length > 1 ? "true" : undefined}>
|
||||
<Show when={canSpeakReasoning()}>
|
||||
<SpeechActionButton
|
||||
class="message-action-button"
|
||||
|
|
@ -1671,7 +1801,7 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||
|
||||
<button
|
||||
type="button"
|
||||
class="message-action-button"
|
||||
class="message-action-button message-reasoning-primary-action"
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
|
@ -1713,6 +1843,13 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||
</button>
|
||||
</Show>
|
||||
|
||||
<ActionOverflowMenu
|
||||
items={actionMenuItems()}
|
||||
label={t("messageItem.actions.more")}
|
||||
triggerClass="message-action-button"
|
||||
minItems={2}
|
||||
/>
|
||||
|
||||
<span class="message-reasoning-time">{timestamp()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { For, Show, createEffect, createSignal, onCleanup } from "solid-js"
|
||||
import { Portal } from "solid-js/web"
|
||||
import { Copy, ListStart, Split, Trash, Undo } from "lucide-solid"
|
||||
import { Copy, ListStart, Split, Trash, Undo, Volume2 } from "lucide-solid"
|
||||
import type { MessageInfo, ClientPart, SDKAssistantMessageV2 } from "../types/message"
|
||||
import { isHiddenSyntheticTextPart, partHasRenderableText } from "../types/message"
|
||||
import type { MessageRecord } from "../stores/message-v2/types"
|
||||
|
|
@ -13,6 +13,7 @@ import { isTauriHost } from "../lib/runtime-env"
|
|||
import type { DeleteHoverState } from "../types/delete-hover"
|
||||
import { useSpeech } from "../lib/hooks/use-speech"
|
||||
import SpeechActionButton from "./speech-action-button"
|
||||
import ActionOverflowMenu, { type ActionOverflowMenuItem } from "./action-overflow-menu"
|
||||
|
||||
function DeleteUpToIcon() {
|
||||
return (
|
||||
|
|
@ -388,6 +389,71 @@ export default function MessageItem(props: MessageItemProps) {
|
|||
return segments.join(" • ")
|
||||
}
|
||||
|
||||
const actionMenuItems = (): ActionOverflowMenuItem[] => {
|
||||
const items: ActionOverflowMenuItem[] = [
|
||||
{
|
||||
key: "copy",
|
||||
label: copyLabel(),
|
||||
icon: <Copy class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
onSelect: handleCopy,
|
||||
},
|
||||
]
|
||||
|
||||
if (canSpeakMessage()) {
|
||||
items.push({
|
||||
key: "speak",
|
||||
label: speech.buttonTitle(),
|
||||
icon: <Volume2 class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
onSelect: () => void speech.toggle(),
|
||||
})
|
||||
}
|
||||
|
||||
if (isUser() && props.onFork) {
|
||||
items.push({
|
||||
key: "fork",
|
||||
label: t("messageItem.actions.fork"),
|
||||
icon: <Split class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
onSelect: () => props.onFork?.(props.record.id),
|
||||
})
|
||||
}
|
||||
|
||||
if (isUser() && props.onRevert) {
|
||||
items.push({
|
||||
key: "revert",
|
||||
label: t("messageItem.actions.revertTitle"),
|
||||
icon: <Undo class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
onSelect: handleRevert,
|
||||
})
|
||||
}
|
||||
|
||||
if (props.showDeleteMessage) {
|
||||
items.push(
|
||||
{
|
||||
key: "delete-up-to",
|
||||
label: t("messageItem.actions.deleteMessagesUpTo"),
|
||||
icon: <DeleteUpToIcon />,
|
||||
disabled: !props.onDeleteMessagesUpTo || deletingUpTo(),
|
||||
destructive: true,
|
||||
onMouseEnter: () => props.onDeleteHoverChange?.({ kind: "deleteUpTo", messageId: props.record.id }),
|
||||
onMouseLeave: () => props.onDeleteHoverChange?.({ kind: "none" }),
|
||||
onSelect: () => void handleDeleteUpTo(),
|
||||
},
|
||||
{
|
||||
key: "delete-message",
|
||||
label: deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage"),
|
||||
icon: <Trash class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
disabled: deletingMessage(),
|
||||
destructive: true,
|
||||
onMouseEnter: () => props.onDeleteHoverChange?.({ kind: "message", messageId: props.record.id }),
|
||||
onMouseLeave: () => props.onDeleteHoverChange?.({ kind: "none" }),
|
||||
onSelect: handleDeleteMessage,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -440,7 +506,11 @@ export default function MessageItem(props: MessageItemProps) {
|
|||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="message-item-actions" ref={(el) => (actionsEl = el)}>
|
||||
<div
|
||||
class="message-item-actions"
|
||||
data-action-overflow={actionMenuItems().length > 1 ? "true" : undefined}
|
||||
ref={(el) => (actionsEl = el)}
|
||||
>
|
||||
<Show when={isUser()}>
|
||||
<div class="message-action-group">
|
||||
<button
|
||||
|
|
@ -510,6 +580,12 @@ export default function MessageItem(props: MessageItemProps) {
|
|||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
<ActionOverflowMenu
|
||||
items={actionMenuItems()}
|
||||
label={t("messageItem.actions.more")}
|
||||
triggerClass="message-action-button"
|
||||
minItems={2}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={!isUser()}>
|
||||
<div class="message-action-group">
|
||||
|
|
@ -558,6 +634,12 @@ export default function MessageItem(props: MessageItemProps) {
|
|||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
<ActionOverflowMenu
|
||||
items={actionMenuItems()}
|
||||
label={t("messageItem.actions.more")}
|
||||
triggerClass="message-action-button"
|
||||
minItems={2}
|
||||
/>
|
||||
</Show>
|
||||
<time class="message-timestamp" dateTime={timestampIso()}>{timestamp()}</time>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { createSignal, Show, createEffect, createMemo, onCleanup, type Accessor } from "solid-js"
|
||||
import { ArrowRightSquare, Check, Copy, Hourglass, Loader2, XCircle } from "lucide-solid"
|
||||
import { ArrowRightSquare, Check, Copy, Hourglass, Loader2, Volume2, XCircle } from "lucide-solid"
|
||||
import { stringify as stringifyYaml } from "yaml"
|
||||
import { messageStoreBus } from "../stores/message-v2/bus"
|
||||
import { useTheme } from "../lib/theme"
|
||||
|
|
@ -45,6 +45,7 @@ import { getLogger } from "../lib/logger"
|
|||
import { useSpeech } from "../lib/hooks/use-speech"
|
||||
import SpeechActionButton from "./speech-action-button"
|
||||
import { createFollowScroll } from "../lib/follow-scroll"
|
||||
import ActionOverflowMenu, { type ActionOverflowMenuItem } from "./action-overflow-menu"
|
||||
|
||||
const log = getLogger("session")
|
||||
|
||||
|
|
@ -821,23 +822,62 @@ export default function ToolCall(props: ToolCallProps) {
|
|||
await copyToClipboard(text)
|
||||
}
|
||||
|
||||
const actionMenuItems = (): ActionOverflowMenuItem[] => {
|
||||
const items: ActionOverflowMenuItem[] = []
|
||||
|
||||
if (hasToolInput()) {
|
||||
items.push({
|
||||
key: "toggle-input",
|
||||
label: isToolInputVisible() ? t("toolCall.header.hideInputTitle") : t("toolCall.header.showInputTitle"),
|
||||
icon: <ArrowRightSquare class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
onSelect: () => {
|
||||
if (!expanded()) toggle()
|
||||
const currentlyVisible = isToolInputVisible()
|
||||
setToolInputVisibilityOverride(currentlyVisible ? "hidden" : "expanded")
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
items.push({
|
||||
key: "copy",
|
||||
label: t("toolCall.header.copyTitle"),
|
||||
icon: <Copy class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
onSelect: async () => {
|
||||
const text = headerText()
|
||||
if (!text) return
|
||||
await copyToClipboard(text)
|
||||
},
|
||||
})
|
||||
|
||||
if (canSpeakToolCall()) {
|
||||
items.push({
|
||||
key: "speak",
|
||||
label: speech.buttonTitle(),
|
||||
icon: <Volume2 class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
onSelect: () => void speech.toggle(),
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
const status = () => toolState()?.status || ""
|
||||
|
||||
return (
|
||||
<div
|
||||
<div
|
||||
|
||||
ref={(element) => {
|
||||
ref={(element) => {
|
||||
setToolCallRootEl(element || undefined)
|
||||
}}
|
||||
class={`tool-call ${combinedStatusClass()}`}
|
||||
data-part-type="tool"
|
||||
data-tool-name={toolName()}
|
||||
data-instance-id={props.instanceId}
|
||||
data-session-id={props.sessionId}
|
||||
data-message-id={props.messageId}
|
||||
data-part-id={toolCallIdentifier()}
|
||||
>
|
||||
<div class="tool-call-header">
|
||||
data-session-id={props.sessionId}
|
||||
data-message-id={props.messageId}
|
||||
data-part-id={toolCallIdentifier()}
|
||||
>
|
||||
<div class="tool-call-header" data-action-overflow={actionMenuItems().length > 1 ? "true" : undefined}>
|
||||
<button
|
||||
type="button"
|
||||
class="tool-call-header-toggle"
|
||||
|
|
@ -886,6 +926,13 @@ export default function ToolCall(props: ToolCallProps) {
|
|||
/>
|
||||
</Show>
|
||||
|
||||
<ActionOverflowMenu
|
||||
items={actionMenuItems()}
|
||||
label={t("messageItem.actions.more")}
|
||||
triggerClass="tool-call-header-copy"
|
||||
minItems={2}
|
||||
/>
|
||||
|
||||
<ToolStatusIndicator status={status} />
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ export const messagingMessages = {
|
|||
"messageItem.actions.copy": "Copy",
|
||||
"messageItem.actions.copyTitle": "Copy message",
|
||||
"messageItem.actions.copied": "Copied!",
|
||||
"messageItem.actions.more": "More actions",
|
||||
"messageItem.actions.speak": "Speak message",
|
||||
"messageItem.actions.generatingSpeech": "Generating speech",
|
||||
"messageItem.actions.stopSpeech": "Stop playback",
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ export const messagingMessages = {
|
|||
"messageItem.actions.copy": "Copiar",
|
||||
"messageItem.actions.copyTitle": "Copiar mensaje",
|
||||
"messageItem.actions.copied": "¡Copiado!",
|
||||
"messageItem.actions.more": "Más acciones",
|
||||
"messageItem.actions.speak": "Reproducir mensaje",
|
||||
"messageItem.actions.generatingSpeech": "Generando audio",
|
||||
"messageItem.actions.stopSpeech": "Detener reproduccion",
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ export const messagingMessages = {
|
|||
"messageItem.actions.copy": "Copier",
|
||||
"messageItem.actions.copyTitle": "Copier le message",
|
||||
"messageItem.actions.copied": "Copié !",
|
||||
"messageItem.actions.more": "Plus d'actions",
|
||||
"messageItem.actions.speak": "Lire le message",
|
||||
"messageItem.actions.generatingSpeech": "Generation de l'audio",
|
||||
"messageItem.actions.stopSpeech": "Arreter la lecture",
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ export const messagingMessages = {
|
|||
"messageItem.actions.copy": "העתק",
|
||||
"messageItem.actions.copyTitle": "העתק הודעה",
|
||||
"messageItem.actions.copied": "הועתק!",
|
||||
"messageItem.actions.more": "פעולות נוספות",
|
||||
"messageItem.actions.speak": "השמע הודעה",
|
||||
"messageItem.actions.generatingSpeech": "יוצר אודיו",
|
||||
"messageItem.actions.stopSpeech": "עצור ניגון",
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ export const messagingMessages = {
|
|||
"messageItem.actions.copy": "コピー",
|
||||
"messageItem.actions.copyTitle": "メッセージをコピー",
|
||||
"messageItem.actions.copied": "コピーしました!",
|
||||
"messageItem.actions.more": "その他の操作",
|
||||
"messageItem.actions.speak": "メッセージを読み上げ",
|
||||
"messageItem.actions.generatingSpeech": "音声を生成中",
|
||||
"messageItem.actions.stopSpeech": "再生を停止",
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ export const messagingMessages = {
|
|||
"messageItem.actions.copy": "Копировать",
|
||||
"messageItem.actions.copyTitle": "Копировать сообщение",
|
||||
"messageItem.actions.copied": "Скопировано!",
|
||||
"messageItem.actions.more": "Больше действий",
|
||||
"messageItem.actions.speak": "Озвучить сообщение",
|
||||
"messageItem.actions.generatingSpeech": "Генерация аудио",
|
||||
"messageItem.actions.stopSpeech": "Остановить воспроизведение",
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ export const messagingMessages = {
|
|||
"messageItem.actions.copy": "复制",
|
||||
"messageItem.actions.copyTitle": "复制消息",
|
||||
"messageItem.actions.copied": "已复制!",
|
||||
"messageItem.actions.more": "更多操作",
|
||||
"messageItem.actions.speak": "朗读消息",
|
||||
"messageItem.actions.generatingSpeech": "正在生成语音",
|
||||
"messageItem.actions.stopSpeech": "停止播放",
|
||||
|
|
|
|||
|
|
@ -70,3 +70,62 @@
|
|||
.dropdown-icon-accent {
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.action-overflow-trigger {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.action-overflow-content {
|
||||
min-width: 12rem;
|
||||
padding: 0.25rem;
|
||||
border: 1px solid var(--border-base);
|
||||
border-radius: 0;
|
||||
background-color: var(--surface-base);
|
||||
box-shadow: var(--popover-shadow);
|
||||
color: var(--text-primary);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.action-overflow-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
min-height: 2rem;
|
||||
padding: 0.4rem 0.5rem;
|
||||
border-radius: 0;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.2;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.action-overflow-item:hover,
|
||||
.action-overflow-item[data-highlighted] {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
.action-overflow-item[data-disabled] {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-overflow-item[data-destructive="true"] {
|
||||
color: var(--status-error);
|
||||
}
|
||||
|
||||
.action-overflow-item-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.action-overflow-item-label {
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,6 +113,18 @@
|
|||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.message-item-actions > .action-overflow-trigger {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.session-center-column[data-session-center-width="narrow"] .message-item-actions[data-action-overflow="true"] .message-action-group {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.session-center-column[data-session-center-width="narrow"] .message-item-actions[data-action-overflow="true"] > .action-overflow-trigger {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.message-timestamp {
|
||||
@apply text-[11px] text-[var(--text-muted)];
|
||||
}
|
||||
|
|
@ -433,10 +445,22 @@
|
|||
.message-reasoning-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
gap: 0;
|
||||
padding: 0.25rem 0.6rem 0.25rem 0;
|
||||
}
|
||||
|
||||
.message-reasoning-actions > .action-overflow-trigger {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.session-center-column[data-session-center-width="narrow"] .message-reasoning-actions[data-action-overflow="true"] > .message-action-button:not(.message-reasoning-primary-action):not(.action-overflow-trigger) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.session-center-column[data-session-center-width="narrow"] .message-reasoning-actions[data-action-overflow="true"] > .action-overflow-trigger {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.message-reasoning-toggle:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,18 @@
|
|||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.tool-call-header-label > .action-overflow-trigger {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.session-center-column[data-session-center-width="narrow"] .tool-call-header-label[data-action-overflow="true"] .tool-call-header-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.session-center-column[data-session-center-width="narrow"] .tool-call-header-label[data-action-overflow="true"] > .action-overflow-trigger {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.tool-call-header-label .tool-call-icon {
|
||||
@apply text-base;
|
||||
}
|
||||
|
|
@ -151,6 +163,19 @@
|
|||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.tool-call-header > .action-overflow-trigger {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.session-center-column[data-session-center-width="narrow"] .tool-call-header[data-action-overflow="true"] > .tool-call-header-input,
|
||||
.session-center-column[data-session-center-width="narrow"] .tool-call-header[data-action-overflow="true"] > .tool-call-header-copy:not(.action-overflow-trigger) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.session-center-column[data-session-center-width="narrow"] .tool-call-header[data-action-overflow="true"] > .action-overflow-trigger {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.tool-call-header-status {
|
||||
@apply inline-flex items-center justify-center;
|
||||
font-size: 0.95rem;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue