mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-06 08:06:43 +08:00
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:
File diff suppressed because it is too large
Load Diff
@@ -8,11 +8,13 @@ const panelOpen = ref(false)
|
|||||||
const thinking = ref(false)
|
const thinking = ref(false)
|
||||||
const entryRef = ref<AgentAssistantEntryRef | null>(null)
|
const entryRef = ref<AgentAssistantEntryRef | null>(null)
|
||||||
|
|
||||||
|
// 打开 Agent 面板并清空入口预览气泡。
|
||||||
function openPanel() {
|
function openPanel() {
|
||||||
panelOpen.value = true
|
panelOpen.value = true
|
||||||
entryRef.value?.clearBubbles()
|
entryRef.value?.clearBubbles()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 在面板关闭时展示助手回复预览。
|
||||||
function handleAssistantPreview(value: string) {
|
function handleAssistantPreview(value: string) {
|
||||||
if (panelOpen.value) return
|
if (panelOpen.value) return
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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]
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user