diff --git a/packages/stage-pages/src/pages/devtools/context-flow.vue b/packages/stage-pages/src/pages/devtools/context-flow.vue index 06894ac2c..02efd110f 100644 --- a/packages/stage-pages/src/pages/devtools/context-flow.vue +++ b/packages/stage-pages/src/pages/devtools/context-flow.vue @@ -5,6 +5,7 @@ import type { ChatStreamEvent, ContextMessage } from '@proj-airi/stage-ui/types/ import { errorMessageFrom } from '@moeru/std' import { ContextUpdateStrategy } from '@proj-airi/server-sdk' import { Section } from '@proj-airi/stage-ui/components' +import { useCharacterStore } from '@proj-airi/stage-ui/stores/character' import { useCharacterOrchestratorStore } from '@proj-airi/stage-ui/stores/character-orchestrator' import { CHAT_STREAM_CHANNEL_NAME, CONTEXT_CHANNEL_NAME, useChatStore } from '@proj-airi/stage-ui/stores/chat' import { useModsServerChannelStore } from '@proj-airi/stage-ui/stores/mods/api/channel-server' @@ -29,6 +30,7 @@ interface FlowEntry { } const chatStore = useChatStore() +const characterStore = useCharacterStore() const characterOrchestratorStore = useCharacterOrchestratorStore() const serverChannelStore = useModsServerChannelStore() @@ -67,6 +69,19 @@ const directionOptions = [ const directionFilter = ref<'all' | FlowDirection>('all') const previewMaxLength = 420 +interface SparkNotifyEntryState { + eventId: string + sparkId?: string + handling: boolean + commands: WebSocketEvents['spark:command'][] + reaction: string + startedAt: number + endedAt?: number + error?: string +} + +const sparkNotifyStates = ref>(new Map()) + interface PreviewItem { label: string value: string @@ -258,6 +273,57 @@ function buildPreviewItems(entry: FlowEntry): PreviewItem[] { return items } +function getSparkNotifyReaction(eventId: string) { + const reactions = characterStore.reactions + for (let index = reactions.length - 1; index >= 0; index -= 1) { + const reaction = reactions[index] + if (reaction.sourceEventId === eventId) + return reaction + } + return undefined +} + +function getSparkNotifyEntryState(entry: FlowEntry) { + if (entry.type !== 'spark:notify') + return undefined + const payload = getPayloadData(entry) as { id?: string } | undefined + if (!payload?.id) + return undefined + return sparkNotifyStates.value.get(payload.id) +} + +function setSparkNotifyState(nextState: SparkNotifyEntryState) { + const nextMap = new Map(sparkNotifyStates.value) + nextMap.set(nextState.eventId, nextState) + sparkNotifyStates.value = nextMap +} + +function updateSparkNotifyState(eventId: string, updater: (state: SparkNotifyEntryState) => SparkNotifyEntryState) { + const current = sparkNotifyStates.value.get(eventId) + if (!current) + return + setSparkNotifyState(updater(current)) +} + +function buildSparkCommandPreview(command: WebSocketEvents['spark:command']): PreviewItem[] { + const items: PreviewItem[] = [] + if (command.intent) + items.push({ label: 'Intent', value: formatPreviewValue(command.intent) }) + if (command.priority) + items.push({ label: 'Priority', value: formatPreviewValue(command.priority) }) + if (command.interrupt !== undefined) + items.push({ label: 'Interrupt', value: formatPreviewValue(command.interrupt) }) + if (command.destinations?.length) + items.push({ label: 'Destinations', value: formatPreviewValue(formatDestinations(command.destinations)) }) + if (command.ack) + items.push({ label: 'Ack', value: formatPreviewValue(command.ack) }) + if (command.guidance !== undefined) + items.push({ label: 'Guidance', value: formatPreviewValue(command.guidance) }) + if (command.contexts !== undefined) + items.push({ label: 'Contexts', value: formatPreviewValue(command.contexts) }) + return items +} + function summarizeServerEvent(event: { type: string, data: Record }) { switch (event.type) { case 'module:announce': @@ -454,7 +520,26 @@ async function sendTestSparkNotify() { }) try { + setSparkNotifyState({ + eventId: notify.id, + sparkId: notify.eventId, + handling: true, + commands: [], + reaction: '', + startedAt: Date.now(), + }) + const result = await characterOrchestratorStore.handleSparkNotify(simulatedEvent) + const reaction = getSparkNotifyReaction(notify.id) + updateSparkNotifyState(notify.id, current => ({ + ...current, + sparkId: notify.eventId, + handling: false, + commands: result?.commands ?? [], + reaction: reaction?.message ?? '', + endedAt: Date.now(), + })) + if (result?.commands?.length) { for (const command of result.commands) { serverChannelStore.send({ @@ -466,6 +551,12 @@ async function sendTestSparkNotify() { } catch (error) { toast(`Error handling spark:notify: ${errorMessageFrom(error)}`) + updateSparkNotifyState(notify.id, current => ({ + ...current, + handling: false, + endedAt: Date.now(), + error: errorMessageFrom(error), + })) } } @@ -510,6 +601,21 @@ onMounted(() => { for (const type of serverEventTypes) { cleanupFns.push(serverChannelStore.onEvent(type, (event) => { + if (event.type === 'spark:notify') { + const eventId = (event as WebSocketBaseEvent<'spark:notify', WebSocketEvents['spark:notify']>).data?.id + if (eventId && !sparkNotifyStates.value.has(eventId)) { + const sparkId = (event as WebSocketBaseEvent<'spark:notify', WebSocketEvents['spark:notify']>).data?.eventId + setSparkNotifyState({ + eventId, + sparkId, + handling: true, + commands: [], + reaction: '', + startedAt: Date.now(), + }) + } + } + pushEntry({ direction: 'incoming', channel: 'server', @@ -656,6 +762,22 @@ watch(() => entries.value.length, async () => { streamContainer.value.scrollTop = 0 }) +watch(() => characterStore.reactions.length, () => { + for (const state of sparkNotifyStates.value.values()) { + if (state.reaction) + continue + const reaction = getSparkNotifyReaction(state.eventId) + if (!reaction) + continue + updateSparkNotifyState(state.eventId, current => ({ + ...current, + reaction: reaction.message, + handling: false, + endedAt: current.endedAt ?? Date.now(), + })) + } +}) + watch(maxEntriesValue, () => { if (entries.value.length > maxEntriesValue.value) entries.value.splice(0, entries.value.length - maxEntriesValue.value) @@ -758,7 +880,12 @@ onUnmounted(() => { :input-class="['font-mono', 'min-h-44', 'overflow-hidden']" />
-
@@ -828,6 +955,123 @@ onUnmounted(() => { {{ entry.summary }} +
+
+
+
+ + + {{ getSparkNotifyEntryState(entry)?.handling ? 'Handling spark:notify...' : 'spark:notify handled' }} + +
+ + {{ + Math.max( + 0, + (getSparkNotifyEntryState(entry)?.endedAt ?? 0) + - (getSparkNotifyEntryState(entry)?.startedAt ?? 0), + ) + }}ms + +
+
+ eventId: {{ getSparkNotifyEntryState(entry)?.eventId }} ยท sparkId: {{ getSparkNotifyEntryState(entry)?.sparkId ?? '-' }} +
+
+ {{ getSparkNotifyEntryState(entry)?.error }} +
+
+
+ Output text +
+
+{{ getSparkNotifyEntryState(entry)?.reaction || '-' }}
+                    
+
+
+
+ Commands +
+
+
+
+ Command {{ index + 1 }} +
+
+
+
+ {{ item.label }} +
+
+{{ item.value || '-' }}
+                            
+
+
+
+ No command details available. +
+
+
+
+ No commands returned. +
+
+
+
+ No spark:notify handling data yet. +
+
+
Preview