fix(chat): float approvals above composer
This commit is contained in:
1 parent
efb44f9340
commit
1b52df0603
5 files changed
+321
-127
No files matched your search
@@ -14,7 +14,7 @@ import { SourceModeSubmenu, SourceReferenceFilesSubmenu } from '../workbuddy/com
|
||||
import type { ResourceEntry } from '@shared/resources'
|
||||
import { ComposerControls, type ComposerSendPayload } from '../workbuddy/composer/ComposerControls'
|
||||
import { ComposerBranchLabel, ContextUsageRing } from '../workbuddy/composer/context-info/ComposerContextInfo'
|
||||
import { QuestionFloating } from '../workbuddy/conversation'
|
||||
import { PermissionConfirm, QuestionFloating } from '../workbuddy/conversation'
|
||||
import type { AgentMode, ChatAttachment, MessageQuote, PermissionMode, UserContentBlock } from '@shared/conversation'
|
||||
import type { UserMessageEditContent } from '../workbuddy/conversation/UserMessageEditor'
|
||||
import type { ChatMessage } from '@shared/types'
|
||||
@@ -255,7 +255,8 @@ export default function ChatPane({ rightVisible, onToggleRight, onOpenSettings,
|
||||
return preceding ? send(preceding.content, { attachments: preceding.attachments, resources: preceding.resources, quotes: preceding.quotes, userContent: preceding.userContent, permissionMode, fromMessageId: message.id, action: 'regenerate' }) : Promise.resolve()
|
||||
}
|
||||
const lastAssistantId = messages.findLast((message) => message.role === 'assistant')?.id
|
||||
const question = messages.flatMap((message) => message.interactions ?? []).find((item) => item.kind === 'question' && ['pending', 'responding', 'error'].includes(item.status))
|
||||
const activeInteraction = messages.flatMap((message) => message.interactions ?? []).findLast((item) =>
|
||||
(item.kind === 'question' || item.kind === 'approval') && ['pending', 'responding', 'error'].includes(item.status))
|
||||
|
||||
const bindDraftWorkspace = (ownerDraftId: number, workspaceId = useSettingsStore.getState().selectedWorkspaceId): void => {
|
||||
const settings = useSettingsStore.getState()
|
||||
@@ -309,11 +310,21 @@ export default function ChatPane({ rightVisible, onToggleRight, onOpenSettings,
|
||||
}}
|
||||
userContent={entry.userContent}
|
||||
onUserContentChange={userContent => setDrafts(all => ({ ...all, [draftKey]: { ...(all[draftKey] ?? { text: '', attachments: [] }), userContent } }))}
|
||||
floatingSlot={question?.kind === 'question' && <QuestionFloating key={question.id} config={{ questions: question.questions, title: question.title, toolCallId: question.toolCallId }}
|
||||
responding={question.status === 'responding'} error={question.error}
|
||||
floatingSlot={activeInteraction?.kind === 'question' ? <QuestionFloating key={activeInteraction.id} config={{ questions: activeInteraction.questions, title: activeInteraction.title, toolCallId: activeInteraction.toolCallId }}
|
||||
responding={activeInteraction.status === 'responding'} error={activeInteraction.error}
|
||||
labels={{ title: '需要你确认', skip: '跳过', next: '下一题', send: '发送回答', customAnswer: '输入其他回答', skipAll: '跳过全部', previousQuestion: '上一题', nextQuestion: '下一题', dismiss: '收起', notAnswered: '未回答' }}
|
||||
onSelect={(_text, answer) => respond(question.id, { kind: 'question', answer })}
|
||||
onSkip={() => void respond(question.id, { kind: 'question', answer: { questions: question.questions.map((item) => ({ id: item.id, question: item.question, answers: [] })) }, skipped: true }).catch(() => undefined)} />}
|
||||
onSelect={(_text, answer) => respond(activeInteraction.id, { kind: 'question', answer })}
|
||||
onSkip={() => void respond(activeInteraction.id, { kind: 'question', answer: { questions: activeInteraction.questions.map((item) => ({ id: item.id, question: item.question, answers: [] })) }, skipped: true }).catch(() => undefined)} />
|
||||
: activeInteraction?.kind === 'approval' ? <PermissionConfirm key={activeInteraction.id} className="permission-floating"
|
||||
pendingId={activeInteraction.id} toolName={activeInteraction.toolCallId} title={activeInteraction.title}
|
||||
options={activeInteraction.options} status={activeInteraction.status} error={activeInteraction.error}
|
||||
labels={{ confirm: '允许一次', alwaysAllow: '始终允许', reject: '拒绝', working: '提交中', approved: '已允许', rejected: '已拒绝', cancelled: '已取消', failed: '提交失败' }}
|
||||
resolve={(id, decision) => respond(id, { kind: 'approval', optionId: decision.optionId })}
|
||||
reject={(id) => {
|
||||
const option = activeInteraction.options.find((item) => item.kind.startsWith('reject'))
|
||||
return option ? respond(id, { kind: 'approval', optionId: option.optionId }) : Promise.reject(new Error('审批选项不可用'))
|
||||
}} />
|
||||
: undefined}
|
||||
onResourcesChange={(resources) => setDrafts((all) => ({ ...all, [draftKey]: { ...(all[draftKey] ?? { text: '', attachments: [] }), resources } }))}
|
||||
quotes={entry.quotes ?? []}
|
||||
onRemoveQuote={removeQuote}
|
||||
@@ -379,7 +390,7 @@ export default function ChatPane({ rightVisible, onToggleRight, onOpenSettings,
|
||||
}}>
|
||||
<div className="chat-thread cr-message-list__content"><div className="cr-document">
|
||||
{messages.map((message) => (
|
||||
<MessageBubble key={message.id} message={message} sessionId={currentId} onQuote={quoteTable} darkMode={darkMode} onRespond={respond}
|
||||
<MessageBubble key={message.id} message={message} sessionId={currentId} onQuote={quoteTable} darkMode={darkMode}
|
||||
quotedSourceIds={quotedSourceIds}
|
||||
artifactCount={!running && !sending && message.id === lastAssistantId ? artifactCount : 0}
|
||||
onShowArtifacts={() => window.dispatchEvent(new CustomEvent('ycode:open-artifacts', { detail: { sessionId: currentId } }))}
|
||||
|
||||
@@ -6,8 +6,7 @@ import { CompletedFold, LiveEventDisclosure } from '../workbuddy/conversation/Co
|
||||
import type { CompletedFoldToolCallClassification } from '../workbuddy/conversation/completed-fold'
|
||||
import { resolveSourceToolIcon } from '../workbuddy/conversation/SourceToolIcons'
|
||||
import { SourceToolHeader } from '../workbuddy/conversation/SourceToolHeader'
|
||||
import { getCapturedToolKind } from '../workbuddy/conversation/ToolCallRenderer'
|
||||
import { findToolApproval, isActiveToolApproval, sourcePermissionMetadata, toolApprovalOption, type ToolApprovalControl } from '../workbuddy/conversation/tool-approval'
|
||||
import { findToolApproval, isActiveToolApproval } from '../workbuddy/conversation/tool-approval'
|
||||
import { sourceConversationLabel, type SourceConversationLabelKey } from '../workbuddy/conversation/source-labels'
|
||||
import { computeSourceFoldSummary } from '../workbuddy/conversation/recovered-fold-summary/index.mjs'
|
||||
import { Fragment, useState, type ReactNode } from 'react'
|
||||
@@ -15,11 +14,11 @@ import type { MessageActionToolbarProps } from '../workbuddy/message-actions/Mes
|
||||
import { MessageActionToolbar } from '../workbuddy/message-actions/MessageActionToolbar'
|
||||
import { UserMessageToolbar } from '../workbuddy/message-actions/UserMessageToolbar'
|
||||
import { UserMessageEditor, type UserMessageEditContent } from '../workbuddy/conversation/UserMessageEditor'
|
||||
import { PermissionConfirm, QuestionAnswerTranscript, ThinkingBlock, PlanCreateBlock, TaskBlock, ToolCallRenderer, type ConversationToolCall } from '../workbuddy/conversation'
|
||||
import { QuestionAnswerTranscript, ThinkingBlock, PlanCreateBlock, TaskBlock, ToolCallRenderer, type ConversationToolCall } from '../workbuddy/conversation'
|
||||
import { conversationLabel } from '../workbuddy/conversation/labels'
|
||||
import { downloadInHost, openPathInHost } from '../workbuddy/host'
|
||||
import { openArtifactPreview } from '../stores/resources'
|
||||
import type { InteractionResponse, MessageQuote, PlanContent } from '@shared/conversation'
|
||||
import type { MessageQuote, PlanContent } from '@shared/conversation'
|
||||
import MarkdownContent from './MarkdownContent'
|
||||
import StreamingFooter from '../workbuddy/conversation/StreamingFooter'
|
||||
import StreamingTurnDuration from '../workbuddy/conversation/StreamingTurnDuration'
|
||||
@@ -41,7 +40,6 @@ interface Props {
|
||||
onStartEdit?(message: ChatMessage): void
|
||||
onCancelEdit?(): void
|
||||
onSubmitEdit?(message: ChatMessage, text: string, edited?: UserMessageEditContent): Promise<void>
|
||||
onRespond?(id: string, response: InteractionResponse): Promise<void>
|
||||
sessionId?: string | null
|
||||
onBuildPlan?(plan: PlanContent): Promise<void>
|
||||
buildingPlanId?: string
|
||||
@@ -49,7 +47,7 @@ interface Props {
|
||||
onShowArtifacts?(): void
|
||||
}
|
||||
|
||||
export default function MessageBubble({ message, assistantAvatar, onQuote, quotedSourceIds, darkMode, actions, editingMessageId, onStartEdit, onCancelEdit, onSubmitEdit, onRespond, sessionId, onBuildPlan, buildingPlanId, artifactCount = 0, onShowArtifacts }: Props): React.JSX.Element {
|
||||
export default function MessageBubble({ message, assistantAvatar, onQuote, quotedSourceIds, darkMode, actions, editingMessageId, onStartEdit, onCancelEdit, onSubmitEdit, sessionId, onBuildPlan, buildingPlanId, artifactCount = 0, onShowArtifacts }: Props): React.JSX.Element {
|
||||
const [actionError, setActionError] = useState<string | null>(null)
|
||||
const [liveEventsExpanded, setLiveEventsExpanded] = useState(false)
|
||||
const perform = (action: () => Promise<unknown>): void => {
|
||||
@@ -81,14 +79,12 @@ export default function MessageBubble({ message, assistantAvatar, onQuote, quote
|
||||
const parts = getMessageParts(message)
|
||||
const tools = (message.tools ?? []).map(tool => tool.metadata?.importedHistory ? projectWorkBuddyHistoryTool(tool)
|
||||
: tool.metadata?.capability === 'mcp' ? { ...tool, name: 'mcp_call_tool', args: { serverName: tool.metadata.serverName, toolName: tool.metadata.toolName, arguments: tool.args } } : tool)
|
||||
const inlineApprovals = new Map(tools.flatMap(tool => {
|
||||
const kind = getCapturedToolKind(tool.name)
|
||||
const activeApprovals = new Map(tools.flatMap(tool => {
|
||||
const request = findToolApproval(message.interactions, tool.id)
|
||||
if (!onRespond || !request || !['write-file', 'command'].includes(kind ?? '') ||
|
||||
!toolApprovalOption(request.options, 'allow') || !toolApprovalOption(request.options, 'reject')) return []
|
||||
return [[tool.id, request] as const]
|
||||
return request && isActiveToolApproval(request) ? [[tool.id, request] as const] : []
|
||||
}))
|
||||
const inlineApprovalIds = new Set([...inlineApprovals.values()].map(request => request.id))
|
||||
const activeQuestionIds = new Set((message.interactions ?? []).flatMap(interaction =>
|
||||
interaction.kind === 'question' && ['pending', 'responding', 'error'].includes(interaction.status) ? [interaction.toolCallId] : []))
|
||||
const classifyTool = (part: Extract<MessagePart, { type: 'tool' }>): CompletedFoldToolCallClassification | undefined => {
|
||||
const name = tools.find(tool => tool.id === part.toolId)?.name ?? ''
|
||||
if (['show_widget', 'visualize:show_widget', 'visualizer:show_widget'].includes(name)) return { role: 'show-widget' }
|
||||
@@ -98,42 +94,24 @@ export default function MessageBubble({ message, assistantAvatar, onQuote, quote
|
||||
const renderTool = (id: string): React.ReactNode => {
|
||||
const tool = tools.find((item) => item.id === id)
|
||||
if (!tool) return null
|
||||
if (activeQuestionIds.has(tool.id)) return null
|
||||
const nativeResult = tool.result ?? tool.output ?? tool.summary
|
||||
const result = nativeResult && typeof nativeResult === 'object' && 'result' in nativeResult ? nativeResult : { result: nativeResult }
|
||||
const request = inlineApprovals.get(id)
|
||||
const activeRequest = request && isActiveToolApproval(request) ? request : undefined
|
||||
const approval: ToolApprovalControl | undefined = activeRequest && onRespond ? {
|
||||
status: activeRequest.status as ToolApprovalControl['status'],
|
||||
onDecision: decision => {
|
||||
const option = toolApprovalOption(activeRequest.options, decision)
|
||||
if (option) perform(() => onRespond(activeRequest.id, { kind: 'approval', optionId: option.optionId }))
|
||||
}
|
||||
} : undefined
|
||||
const view: ConversationToolCall = { id: tool.id, name: tool.name, status: activeRequest ? 'pending' : tool.status === 'ok' ? 'success' : tool.status,
|
||||
const activeApproval = activeApprovals.get(tool.id)
|
||||
const view: ConversationToolCall = { id: tool.id, name: tool.name, status: activeApproval ? 'pending' : tool.status === 'ok' ? 'success' : tool.status,
|
||||
intent: tool.intent,
|
||||
args: tool.args && typeof tool.args === 'object' ? tool.args as Record<string, unknown> : { input: tool.argsRaw },
|
||||
result, output: tool.metadata?.importedHistory && ['askuserquestion', 'todo_write'].includes(tool.name) ? undefined : tool.output,
|
||||
metaData: activeRequest ? sourcePermissionMetadata(tool.metadata, activeRequest) : tool.metadata,
|
||||
metaData: tool.metadata,
|
||||
isFinished: tool.status !== 'running', isLastContent: parts.at(-1)?.type === 'tool' && (parts.at(-1) as { toolId: string }).toolId === id }
|
||||
const plan = planFromTool(tool.result, tool.args, tool.id, tool.name)
|
||||
const decideTask = async (index: number, toolCallId: string, data: { toolInfo?: Array<{ permissionRequestId?: string }> } | undefined, approve: boolean): Promise<void> => {
|
||||
const binding = data?.toolInfo?.[index]?.permissionRequestId || toolCallId
|
||||
const interaction = message.interactions?.find((item) => item.kind === 'approval' && [item.id, item.requestId, item.toolCallId].includes(binding))
|
||||
if (interaction?.kind !== 'approval' || !onRespond) throw new Error('审批请求已结束或未连接')
|
||||
const option = interaction.options.find((item) => approve ? item.kind.startsWith('allow') : item.kind.startsWith('reject'))
|
||||
if (!option) throw new Error('审批选项不可用')
|
||||
await onRespond(interaction.id, { kind: 'approval', optionId: option.optionId })
|
||||
}
|
||||
const hasApproval = !!onRespond && message.interactions?.some((item) => item.kind === 'approval' && ['pending', 'error', 'responding'].includes(item.status))
|
||||
return <ToolCallRenderer key={id} tool={view} approval={approval} sessionId={sessionId ?? undefined} renderMarkdown={(text) => <MarkdownContent content={text} darkMode={darkMode} />}
|
||||
return <ToolCallRenderer key={id} tool={view} sessionId={sessionId ?? undefined} renderMarkdown={(text) => <MarkdownContent content={text} darkMode={darkMode} />}
|
||||
onOpenFile={(request) => request.artifact
|
||||
? openArtifactPreview({ kind: 'artifact', path: request.filePath, name: request.title || request.artifact.title || request.filePath, content: request.content ?? '', artifact: request.artifact })
|
||||
: request.artifactType && request.content !== undefined
|
||||
? openArtifactPreview({ kind: 'artifact', path: request.filePath, name: request.title || request.filePath, content: request.content, artifactType: request.artifactType })
|
||||
: openPathInHost(request.filePath, 'file', request.startLine ? { start: request.startLine, end: request.endLine ?? request.startLine } : undefined)}
|
||||
onOpenLink={(url) => perform(() => window.ycode.resources.openExternal(url))}
|
||||
onTaskApprove={hasApproval ? (index, toolCallId, data) => decideTask(index, toolCallId, data, true) : undefined}
|
||||
onTaskReject={hasApproval ? (index, toolCallId, data) => decideTask(index, toolCallId, data, false) : undefined}
|
||||
onPlanTaskClick={(request) => viewPlan(plan, request.taskId)} onPlanView={() => viewPlan(plan)} onPlanDownload={() => downloadPlan(plan)}
|
||||
onPlanBuild={onBuildPlan ? (request) => perform(() => onBuildPlan({ id: request.planId, title: request.planName, steps: request.tasks.map((task) => ({ id: String(task.id), text: task.title, status: 'pending' })) })) : undefined}
|
||||
isBuildingPlan={buildingPlanId === plan.id} />
|
||||
@@ -160,10 +138,7 @@ export default function MessageBubble({ message, assistantAvatar, onQuote, quote
|
||||
const part = unit.part as MessagePart
|
||||
return part.type === 'tool' ? tools.filter(tool => tool.id === part.toolId) : []
|
||||
})
|
||||
if (groupedTools.some(tool => {
|
||||
const request = inlineApprovals.get(tool.id)
|
||||
return !!request && isActiveToolApproval(request)
|
||||
})) return <Fragment key={group.key}>{children}</Fragment>
|
||||
if (groupedTools.some(tool => activeApprovals.has(tool.id))) return <Fragment key={group.key}>{children}</Fragment>
|
||||
|
||||
const latestProgressIndex = parts.reduce((latest, part, index) => part.type === 'tool' || part.type === 'reasoning' ? index : latest, -1)
|
||||
if (!group.units.some(unit => unit.index === latestProgressIndex)) return null
|
||||
@@ -174,7 +149,7 @@ export default function MessageBubble({ message, assistantAvatar, onQuote, quote
|
||||
const summaryArgs = latestTool && ['list_dir', 'list_files'].includes(latestTool.name) && !toolArgs.target_directory && !toolArgs.root && typeof toolArgs.path === 'string'
|
||||
? { ...toolArgs, target_directory: toolArgs.path }
|
||||
: toolArgs
|
||||
const pendingConfirmation = !!latestTool && inlineApprovals.has(latestTool.id) && isActiveToolApproval(inlineApprovals.get(latestTool.id)!)
|
||||
const pendingConfirmation = !!latestTool && activeApprovals.has(latestTool.id)
|
||||
const summaryStatus = !latestTool ? 'success' : pendingConfirmation ? 'pending'
|
||||
: latestTool.status === 'ok' ? 'executed'
|
||||
: ['running', 'executing'].includes(latestTool.status) ? 'stream_executing'
|
||||
@@ -195,8 +170,7 @@ export default function MessageBubble({ message, assistantAvatar, onQuote, quote
|
||||
if (part.type !== 'tool') return
|
||||
const tool = tools.find(item => item.id === part.toolId)
|
||||
if (!tool || classifyTool(part)?.role) return
|
||||
const approvalRequest = inlineApprovals.get(tool.id)
|
||||
if (approvalRequest && isActiveToolApproval(approvalRequest)) return
|
||||
if (activeApprovals.has(tool.id) || activeQuestionIds.has(tool.id)) return
|
||||
liveEventParts.push({ part, index: eventIndex })
|
||||
})
|
||||
const activityHeader = <div className="cr-captured-tool cr-streaming-activity" role="status" aria-live="polite" data-activity-tool-id={latestTool?.id}>
|
||||
@@ -248,17 +222,9 @@ export default function MessageBubble({ message, assistantAvatar, onQuote, quote
|
||||
)
|
||||
}} />
|
||||
|
||||
{message.interactions?.map((interaction) => interaction.kind === 'approval' ? inlineApprovalIds.has(interaction.id)
|
||||
? interaction.error && !actionError ? <p key={interaction.id} className="msg__status msg__status--error" role="alert">{interaction.error}</p> : null
|
||||
: <PermissionConfirm key={interaction.id}
|
||||
pendingId={interaction.id} toolName={interaction.toolCallId} title={interaction.title} options={interaction.options}
|
||||
status={interaction.status} error={interaction.error}
|
||||
labels={{ confirm: '允许一次', alwaysAllow: '始终允许', reject: '拒绝', working: '提交中', approved: '已允许', rejected: '已拒绝', cancelled: '已取消', failed: '提交失败' }}
|
||||
resolve={(id, decision) => onRespond?.(id, { kind: 'approval', optionId: decision.optionId })}
|
||||
reject={(id) => {
|
||||
const option = interaction.options.find((item) => item.kind.startsWith('reject'))
|
||||
return option ? onRespond?.(id, { kind: 'approval', optionId: option.optionId }) : undefined
|
||||
}} /> : interaction.answer && !message.tools?.some((tool) => tool.id === interaction.toolCallId && tool.metadata?.questionAnswer) ? <QuestionAnswerTranscript key={interaction.id} questionAnswer={interaction.answer} /> : null)}
|
||||
{message.interactions?.map((interaction) => interaction.kind === 'question' && interaction.answer &&
|
||||
!message.tools?.some((tool) => tool.id === interaction.toolCallId && tool.metadata?.questionAnswer)
|
||||
? <QuestionAnswerTranscript key={interaction.id} questionAnswer={interaction.answer} /> : null)}
|
||||
|
||||
{streaming && <StreamingFooter prepareText={resolvePrepareText(message.prepareStage)} />}
|
||||
|
||||
|
||||
@@ -345,6 +345,177 @@
|
||||
font-size: var(--yc-text-caption-size, 12px);
|
||||
}
|
||||
|
||||
.conversation-input-area__custom-floating {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.conversation-input-area__custom-floating > * {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.conversation-high-credit-approval.permission-floating {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--cb-question-floating-card-border, var(--yc-border-primary, #e6e6e6));
|
||||
border-radius: 24px;
|
||||
background: var(--cb-question-floating-card-bg, var(--yc-bg-primary, #fff));
|
||||
color: var(--cb-question-floating-text-primary, var(--yc-text-primary, #222));
|
||||
box-shadow: 0 6px 16px #00000005, 0 1px 4px #00000008;
|
||||
animation: permission-floating-in .2s ease-out;
|
||||
}
|
||||
|
||||
.permission-floating .conversation-high-credit-approval__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 20px 24px 12px;
|
||||
}
|
||||
|
||||
.permission-floating .conversation-high-credit-approval__title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.permission-floating .conversation-high-credit-approval__title {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--cb-question-floating-text-primary, var(--yc-text-primary, #222));
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.permission-floating .conversation-high-credit-approval__description {
|
||||
margin: 0;
|
||||
color: var(--cb-question-floating-text-secondary, var(--yc-text-secondary, #666));
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.permission-floating .conversation-high-credit-approval__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.permission-floating .conversation-high-credit-approval__option {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
min-height: 44px;
|
||||
padding: 8px 12px;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background-color .15s, opacity .15s;
|
||||
}
|
||||
|
||||
.permission-floating .conversation-high-credit-approval__option:not(:last-child)::after {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
bottom: 0;
|
||||
left: 12px;
|
||||
height: 1px;
|
||||
background: var(--cb-question-floating-hover-bg, var(--yc-bg-hover, #f2f2f2));
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.permission-floating .conversation-high-credit-approval__option:hover:not(:disabled),
|
||||
.permission-floating .conversation-high-credit-approval__option:focus-visible {
|
||||
background: var(--cb-question-floating-hover-bg, var(--yc-bg-hover, #f2f2f2));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.permission-floating .conversation-high-credit-approval__option:disabled {
|
||||
cursor: default;
|
||||
opacity: .45;
|
||||
}
|
||||
|
||||
.permission-floating .conversation-high-credit-approval__option-num {
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 8px;
|
||||
background: var(--cb-question-floating-number-bg, var(--yc-bg-secondary, #f2f2f2));
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.permission-floating .conversation-high-credit-approval__option-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.permission-floating .conversation-high-credit-approval__option-arrow {
|
||||
flex: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--cb-question-floating-text-secondary, var(--yc-text-secondary, #666));
|
||||
opacity: 0;
|
||||
transition: opacity .15s;
|
||||
}
|
||||
|
||||
.permission-floating .conversation-high-credit-approval__option:hover .conversation-high-credit-approval__option-arrow {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.permission-floating .conversation-high-credit-approval__option--reject {
|
||||
color: var(--yc-status-error, #cf222e);
|
||||
}
|
||||
|
||||
.permission-floating .conversation-high-credit-approval__working {
|
||||
padding: 0 24px 16px;
|
||||
color: var(--cb-question-floating-text-secondary, var(--yc-text-secondary, #666));
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.permission-floating .conversation-high-credit-approval__error {
|
||||
margin: 0 24px 8px;
|
||||
color: var(--yc-status-error, #cf222e);
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
@keyframes permission-floating-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.conversation-high-credit-approval.permission-floating {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.wb-composer .cr-input-toolbar {
|
||||
align-items: flex-start;
|
||||
|
||||
Reference in new issue
Block a user