feat: add agent pet types and state management for idle actions

- Introduced new types for agent pet actions and intents in `types.ts`.
- Implemented `useAgentPetMachine` to manage idle action scheduling and execution.
- Added functionality to handle random actions based on interaction states.
This commit is contained in:
jxxghp
2026-07-03 13:19:30 +08:00
parent 6bbac7db7d
commit 9a2af0a412
8 changed files with 1817 additions and 1530 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -8,11 +8,13 @@ const panelOpen = ref(false)
const thinking = ref(false)
const entryRef = ref<AgentAssistantEntryRef | null>(null)
// 打开 Agent 面板并清空入口预览气泡。
function openPanel() {
panelOpen.value = true
entryRef.value?.clearBubbles()
}
// 在面板关闭时展示助手回复预览。
function handleAssistantPreview(value: string) {
if (panelOpen.value) return

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
import CssRobotRenderer from './renderers/CssRobotRenderer.vue'
import type { AgentPetActionName, AgentPetIntent, AgentPetRendererKind } from './types'
const props = withDefaults(
defineProps<{
action?: AgentPetActionName | null
intent?: AgentPetIntent
renderer?: AgentPetRendererKind
thinking?: boolean
}>(),
{
action: null,
intent: 'idle',
renderer: 'css-robot',
thinking: false,
},
)
</script>
<template>
<CssRobotRenderer
v-if="props.renderer === 'css-robot'"
:action="props.action"
:intent="props.intent"
:thinking="props.thinking"
/>
</template>

View File

@@ -0,0 +1,77 @@
import type { AgentPetActionDefinition, AgentPetActionName } from './types'
export const AGENT_PET_RANDOM_ACTION_MIN_DELAY = 8000
export const AGENT_PET_RANDOM_ACTION_MAX_DELAY = 18000
export const AGENT_PET_RANDOM_ACTIONS = ['wave', 'sit', 'eye-roll', 'faint', 'disassemble', 'happy-jump'] as const
export const AGENT_PET_ACTIONS: Record<AgentPetActionName, AgentPetActionDefinition> = {
wave: {
name: 'wave',
intent: 'reaction',
clip: 'agent-fab-action-wave',
duration: 2450,
priority: 1,
interruptible: true,
},
sit: {
name: 'sit',
intent: 'idle',
clip: 'agent-fab-action-sit',
duration: 4200,
priority: 1,
interruptible: true,
},
'eye-roll': {
name: 'eye-roll',
intent: 'reaction',
clip: 'agent-fab-action-eye-roll',
duration: 1900,
priority: 1,
interruptible: true,
},
faint: {
name: 'faint',
intent: 'reaction',
clip: 'agent-fab-action-faint',
duration: 4800,
priority: 1,
interruptible: true,
},
disassemble: {
name: 'disassemble',
intent: 'reaction',
clip: 'agent-fab-action-disassemble',
duration: 6200,
priority: 1,
interruptible: true,
},
'happy-jump': {
name: 'happy-jump',
intent: 'success',
clip: 'agent-fab-action-happy-jump',
duration: 5200,
priority: 1,
interruptible: true,
},
}
/** 获取指定宠物动作的播放时长。 */
export function getAgentPetActionDuration(action: AgentPetActionName) {
return AGENT_PET_ACTIONS[action].duration
}
/** 生成下一次空闲趣味动作的随机等待时间。 */
export function getAgentPetRandomActionDelay() {
return (
AGENT_PET_RANDOM_ACTION_MIN_DELAY +
Math.round(Math.random() * (AGENT_PET_RANDOM_ACTION_MAX_DELAY - AGENT_PET_RANDOM_ACTION_MIN_DELAY))
)
}
/** 从动作池中选择一个不同于上一次的趣味动作。 */
export function pickAgentPetRandomAction(lastAction: AgentPetActionName | null): AgentPetActionName {
const candidates = AGENT_PET_RANDOM_ACTIONS.filter(action => action !== lastAction)
return candidates[Math.floor(Math.random() * candidates.length)] || AGENT_PET_RANDOM_ACTIONS[0]
}

View File

@@ -0,0 +1,44 @@
<script setup lang="ts">
import type { AgentPetActionName, AgentPetIntent } from '../types'
const props = withDefaults(
defineProps<{
action?: AgentPetActionName | null
intent?: AgentPetIntent
thinking?: boolean
}>(),
{
action: null,
intent: 'idle',
thinking: false,
},
)
</script>
<template>
<span
class="agent-assistant-fab__bot"
:data-agent-pet-action="props.action || undefined"
:data-agent-pet-intent="props.intent"
:data-agent-pet-thinking="props.thinking ? 'true' : undefined"
aria-hidden="true"
>
<span class="agent-assistant-fab__antenna" />
<span class="agent-assistant-fab__head">
<span class="agent-assistant-fab__face">
<span class="agent-assistant-fab__eye agent-assistant-fab__eye--left" />
<span class="agent-assistant-fab__eye agent-assistant-fab__eye--right" />
<span class="agent-assistant-fab__smile" />
</span>
</span>
<span class="agent-assistant-fab__body">
<span class="agent-assistant-fab__core" />
</span>
<span class="agent-assistant-fab__arm agent-assistant-fab__arm--left" />
<span class="agent-assistant-fab__arm agent-assistant-fab__arm--right" />
<span class="agent-assistant-fab__leg agent-assistant-fab__leg--left" />
<span class="agent-assistant-fab__leg agent-assistant-fab__leg--right" />
</span>
</template>
<style src="./cssRobot.scss" lang="scss"></style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
export type AgentPetActionName = 'wave' | 'sit' | 'eye-roll' | 'faint' | 'disassemble' | 'happy-jump'
export type AgentPetIntent =
| 'idle'
| 'thinking'
| 'speaking'
| 'notify'
| 'success'
| 'warning'
| 'error'
| 'dragging'
| 'docked'
| 'sleeping'
| 'reaction'
export type AgentPetRendererKind = 'css-robot'
export interface AgentPetActionDefinition {
name: AgentPetActionName
intent: AgentPetIntent
clip: string
duration: number
priority: number
interruptible: boolean
cooldown?: number
next?: AgentPetActionName
}

View File

@@ -0,0 +1,127 @@
import { onScopeDispose, ref, toValue, watch, type MaybeRefOrGetter, type Ref } from 'vue'
import {
getAgentPetActionDuration,
getAgentPetRandomActionDelay,
pickAgentPetRandomAction,
} from './agentPetActions'
import type { AgentPetActionName } from './types'
interface AgentPetMachineOptions {
active: MaybeRefOrGetter<boolean>
thinking: MaybeRefOrGetter<boolean>
docked: Ref<boolean>
dragging: Ref<boolean>
pressed: Ref<boolean>
shouldAutoDock: () => boolean
scheduleAutoDock: () => void
}
/** 管理桌面宠物的空闲动作队列,避免入口组件直接维护动画计时器。 */
export function useAgentPetMachine(options: AgentPetMachineOptions) {
const currentAction = ref<AgentPetActionName | null>(null)
let lastAction: AgentPetActionName | null = null
let randomActionTimer: number | null = null
let actionEndTimer: number | null = null
/** 判断当前交互状态是否适合播放空闲趣味动作。 */
function canRunRandomAction() {
return (
toValue(options.active) &&
!options.docked.value &&
!options.dragging.value &&
!options.pressed.value &&
!toValue(options.thinking)
)
}
/** 清理等待中的随机动作计时器。 */
function clearRandomActionTimer() {
if (randomActionTimer === null) return
window.clearTimeout(randomActionTimer)
randomActionTimer = null
}
/** 清理正在播放动作的结束计时器。 */
function clearActionEndTimer() {
if (actionEndTimer === null) return
window.clearTimeout(actionEndTimer)
actionEndTimer = null
}
/** 停止当前动作并清理全部宠物动作计时器。 */
function clearAction() {
clearRandomActionTimer()
clearActionEndTimer()
currentAction.value = null
}
/** 完成当前动作后恢复空闲态,并按贴边规则决定是否自动收起。 */
function finishRandomAction() {
clearActionEndTimer()
currentAction.value = null
if (!options.docked.value && options.shouldAutoDock()) {
options.scheduleAutoDock()
return
}
scheduleRandomAction()
}
/** 立即播放指定动作,供通知、交互或外部 renderer 编排主动触发。 */
function playAction(action: AgentPetActionName) {
clearRandomActionTimer()
clearActionEndTimer()
currentAction.value = action
actionEndTimer = window.setTimeout(finishRandomAction, getAgentPetActionDuration(action))
}
/** 播放一个随机趣味动作,由 renderer 根据动作名呈现具体动画。 */
function runRandomAction() {
if (!canRunRandomAction()) return
const action = pickAgentPetRandomAction(lastAction)
lastAction = action
playAction(action)
}
/** 安排下一次随机动作,只在宠物可见且空闲时生效。 */
function scheduleRandomAction() {
clearRandomActionTimer()
if (!canRunRandomAction() || currentAction.value || actionEndTimer !== null) return
randomActionTimer = window.setTimeout(() => {
randomActionTimer = null
runRandomAction()
}, getAgentPetRandomActionDelay())
}
/** 根据入口交互状态同步随机动作队列。 */
function syncSchedule() {
if (canRunRandomAction()) {
if (!currentAction.value && randomActionTimer === null && actionEndTimer === null) scheduleRandomAction()
return
}
clearAction()
}
watch(
[() => toValue(options.active), () => toValue(options.thinking), options.docked, options.dragging, options.pressed],
syncSchedule,
)
onScopeDispose(clearAction)
return {
clearAction,
currentAction,
playAction,
scheduleRandomAction,
syncSchedule,
}
}