mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-07-12 16:01:35 +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 entryRef = ref<AgentAssistantEntryRef | null>(null)
|
||||
|
||||
// 打开 Agent 面板并清空入口预览气泡。
|
||||
function openPanel() {
|
||||
panelOpen.value = true
|
||||
entryRef.value?.clearBubbles()
|
||||
}
|
||||
|
||||
// 在面板关闭时展示助手回复预览。
|
||||
function handleAssistantPreview(value: string) {
|
||||
if (panelOpen.value) return
|
||||
|
||||
|
||||
28
src/components/agent/pet/AgentPetStage.vue
Normal file
28
src/components/agent/pet/AgentPetStage.vue
Normal 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>
|
||||
77
src/components/agent/pet/agentPetActions.ts
Normal file
77
src/components/agent/pet/agentPetActions.ts
Normal 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]
|
||||
}
|
||||
44
src/components/agent/pet/renderers/CssRobotRenderer.vue
Normal file
44
src/components/agent/pet/renderers/CssRobotRenderer.vue
Normal 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>
|
||||
1402
src/components/agent/pet/renderers/cssRobot.scss
Normal file
1402
src/components/agent/pet/renderers/cssRobot.scss
Normal file
File diff suppressed because it is too large
Load Diff
27
src/components/agent/pet/types.ts
Normal file
27
src/components/agent/pet/types.ts
Normal 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
|
||||
}
|
||||
127
src/components/agent/pet/useAgentPetMachine.ts
Normal file
127
src/components/agent/pet/useAgentPetMachine.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user