+6
-6
@@ -14,19 +14,19 @@ describe('useContextFlowFormatters', () => {
|
||||
|
||||
it('builds preview items for context updates', () => {
|
||||
const entry: FlowEntry = {
|
||||
channel: 'server',
|
||||
direction: 'incoming',
|
||||
id: 1,
|
||||
timestamp: Date.now(),
|
||||
direction: 'incoming',
|
||||
channel: 'server',
|
||||
type: 'context:update',
|
||||
summary: 'test',
|
||||
payload: {
|
||||
data: {
|
||||
destinations: ['character'],
|
||||
text: 'Hello world',
|
||||
destinations: ['character'],
|
||||
},
|
||||
},
|
||||
searchText: '',
|
||||
summary: 'test',
|
||||
timestamp: Date.now(),
|
||||
type: 'context:update',
|
||||
}
|
||||
|
||||
const items = buildPreviewItems(entry)
|
||||
|
||||
+99
-99
@@ -4,17 +4,93 @@ import type { FlowEntry, PreviewItem } from '../context-flow-types'
|
||||
|
||||
const previewMaxLength = 420
|
||||
|
||||
export function useContextFlowFormatters() {
|
||||
function truncateText(value: string, limit = 160) {
|
||||
if (value.length <= limit)
|
||||
return value
|
||||
return `${value.slice(0, limit)}...`
|
||||
}
|
||||
|
||||
function formatDestinations(destinations: unknown) {
|
||||
if (!destinations)
|
||||
return ''
|
||||
if (Array.isArray(destinations))
|
||||
return destinations.join(', ')
|
||||
if (typeof destinations === 'string')
|
||||
return destinations
|
||||
try {
|
||||
return JSON.stringify(destinations)
|
||||
}
|
||||
catch {
|
||||
return String(destinations)
|
||||
}
|
||||
}
|
||||
|
||||
function getPayloadData(entry: FlowEntry) {
|
||||
const payload = entry.payload as Record<string, any> | undefined
|
||||
if (!payload)
|
||||
return undefined
|
||||
return payload.data ?? payload
|
||||
}
|
||||
|
||||
function getEventSource(entry: FlowEntry) {
|
||||
const payload = entry.payload as Record<string, any> | undefined
|
||||
if (!payload)
|
||||
return undefined
|
||||
return payload.source as string | undefined
|
||||
}
|
||||
|
||||
function summarizeContextUpdate(update: { text?: string, content?: unknown, destinations?: unknown }) {
|
||||
const summaryParts: string[] = []
|
||||
if (update.text) {
|
||||
summaryParts.push(`text="${truncateText(update.text, 120)}"`)
|
||||
}
|
||||
if (update.content !== undefined) {
|
||||
const contentText = typeof update.content === 'string'
|
||||
? update.content
|
||||
: (() => {
|
||||
try {
|
||||
return JSON.stringify(update.content)
|
||||
}
|
||||
catch {
|
||||
return '[unserializable]'
|
||||
}
|
||||
})()
|
||||
summaryParts.push(`content="${truncateText(contentText, 120)}"`)
|
||||
}
|
||||
if (update.destinations !== undefined) {
|
||||
summaryParts.push(`destinations="${truncateText(formatDestinations(update.destinations), 120)}"`)
|
||||
}
|
||||
return summaryParts.join(' ')
|
||||
}
|
||||
|
||||
function toPreviewValue(value: unknown) {
|
||||
if (value === undefined || value === null)
|
||||
return ''
|
||||
if (typeof value === 'string')
|
||||
return value
|
||||
try {
|
||||
return JSON.stringify(value, null, 2)
|
||||
}
|
||||
catch {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
|
||||
function formatPreviewValue(value: unknown) {
|
||||
const text = toPreviewValue(value)
|
||||
if (!text)
|
||||
return ''
|
||||
return truncateText(text, previewMaxLength)
|
||||
}
|
||||
|
||||
function getContextUpdatePreview(entry: FlowEntry) {
|
||||
const candidate = getPayloadData(entry) as Record<string, any> | undefined
|
||||
if (!candidate || (candidate.text === undefined && candidate.content === undefined && candidate.destinations === undefined))
|
||||
return null
|
||||
return {
|
||||
buildPreviewItems,
|
||||
buildSparkCommandPreview,
|
||||
formatDestinations,
|
||||
formatPayload,
|
||||
formatTimestamp,
|
||||
getEventSource,
|
||||
getPayloadData,
|
||||
summarizeContextUpdate,
|
||||
truncateText,
|
||||
text: candidate.text as string | undefined,
|
||||
content: candidate.content as unknown,
|
||||
destinations: candidate.destinations as unknown,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,19 +169,9 @@ function buildSparkCommandPreview(command: WebSocketEvents['spark:command']): Pr
|
||||
return items
|
||||
}
|
||||
|
||||
function formatDestinations(destinations: unknown) {
|
||||
if (!destinations)
|
||||
return ''
|
||||
if (Array.isArray(destinations))
|
||||
return destinations.join(', ')
|
||||
if (typeof destinations === 'string')
|
||||
return destinations
|
||||
try {
|
||||
return JSON.stringify(destinations)
|
||||
}
|
||||
catch {
|
||||
return String(destinations)
|
||||
}
|
||||
function formatTimestamp(value: number) {
|
||||
const date = new Date(value)
|
||||
return date.toLocaleTimeString('en-US', { hour12: false })
|
||||
}
|
||||
|
||||
function formatPayload(payload: unknown) {
|
||||
@@ -121,82 +187,16 @@ function formatPayload(payload: unknown) {
|
||||
}
|
||||
}
|
||||
|
||||
function formatPreviewValue(value: unknown) {
|
||||
const text = toPreviewValue(value)
|
||||
if (!text)
|
||||
return ''
|
||||
return truncateText(text, previewMaxLength)
|
||||
}
|
||||
|
||||
function formatTimestamp(value: number) {
|
||||
const date = new Date(value)
|
||||
return date.toLocaleTimeString('en-US', { hour12: false })
|
||||
}
|
||||
|
||||
function getContextUpdatePreview(entry: FlowEntry) {
|
||||
const candidate = getPayloadData(entry) as Record<string, any> | undefined
|
||||
if (!candidate || (candidate.text === undefined && candidate.content === undefined && candidate.destinations === undefined))
|
||||
return null
|
||||
export function useContextFlowFormatters() {
|
||||
return {
|
||||
content: candidate.content as unknown,
|
||||
destinations: candidate.destinations as unknown,
|
||||
text: candidate.text as string | undefined,
|
||||
buildPreviewItems,
|
||||
buildSparkCommandPreview,
|
||||
formatDestinations,
|
||||
formatPayload,
|
||||
formatTimestamp,
|
||||
getEventSource,
|
||||
getPayloadData,
|
||||
summarizeContextUpdate,
|
||||
truncateText,
|
||||
}
|
||||
}
|
||||
|
||||
function getEventSource(entry: FlowEntry) {
|
||||
const payload = entry.payload as Record<string, any> | undefined
|
||||
if (!payload)
|
||||
return undefined
|
||||
return payload.source as string | undefined
|
||||
}
|
||||
|
||||
function getPayloadData(entry: FlowEntry) {
|
||||
const payload = entry.payload as Record<string, any> | undefined
|
||||
if (!payload)
|
||||
return undefined
|
||||
return payload.data ?? payload
|
||||
}
|
||||
|
||||
function summarizeContextUpdate(update: { content?: unknown, destinations?: unknown, text?: string }) {
|
||||
const summaryParts: string[] = []
|
||||
if (update.text) {
|
||||
summaryParts.push(`text="${truncateText(update.text, 120)}"`)
|
||||
}
|
||||
if (update.content !== undefined) {
|
||||
const contentText = typeof update.content === 'string'
|
||||
? update.content
|
||||
: (() => {
|
||||
try {
|
||||
return JSON.stringify(update.content)
|
||||
}
|
||||
catch {
|
||||
return '[unserializable]'
|
||||
}
|
||||
})()
|
||||
summaryParts.push(`content="${truncateText(contentText, 120)}"`)
|
||||
}
|
||||
if (update.destinations !== undefined) {
|
||||
summaryParts.push(`destinations="${truncateText(formatDestinations(update.destinations), 120)}"`)
|
||||
}
|
||||
return summaryParts.join(' ')
|
||||
}
|
||||
|
||||
function toPreviewValue(value: unknown) {
|
||||
if (value === undefined || value === null)
|
||||
return ''
|
||||
if (typeof value === 'string')
|
||||
return value
|
||||
try {
|
||||
return JSON.stringify(value, null, 2)
|
||||
}
|
||||
catch {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
|
||||
function truncateText(value: string, limit = 160) {
|
||||
if (value.length <= limit)
|
||||
return value
|
||||
return `${value.slice(0, limit)}...`
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import type { WebSocketEvents } from '@proj-airi/server-sdk'
|
||||
|
||||
export type FlowChannel = 'broadcast' | 'chat' | 'devtools' | 'server'
|
||||
type Optional<T> = T | undefined
|
||||
|
||||
export type FlowDirection = 'incoming' | 'outgoing'
|
||||
export type FlowChannel = 'server' | 'broadcast' | 'chat' | 'devtools'
|
||||
|
||||
export interface FlowEntry {
|
||||
channel: FlowChannel
|
||||
direction: FlowDirection
|
||||
id: number
|
||||
timestamp: number
|
||||
direction: FlowDirection
|
||||
channel: FlowChannel
|
||||
type: string
|
||||
summary?: string
|
||||
payload?: unknown
|
||||
searchText: string
|
||||
summary?: string
|
||||
timestamp: number
|
||||
type: string
|
||||
}
|
||||
|
||||
export interface PreviewItem {
|
||||
@@ -20,14 +22,12 @@ export interface PreviewItem {
|
||||
}
|
||||
|
||||
export interface SparkNotifyEntryState {
|
||||
eventId: string
|
||||
sparkId?: string
|
||||
handling: boolean
|
||||
commands: WebSocketEvents['spark:command'][]
|
||||
reaction: string
|
||||
startedAt: number
|
||||
endedAt?: number
|
||||
error?: Optional<string>
|
||||
eventId: string
|
||||
handling: boolean
|
||||
reaction: string
|
||||
sparkId?: string
|
||||
startedAt: number
|
||||
}
|
||||
|
||||
type Optional<T> = T | undefined
|
||||
|
||||
@@ -3,19 +3,19 @@ import type { IOSubsystem } from '@proj-airi/stage-shared'
|
||||
import { IOSubsystems } from '@proj-airi/stage-shared'
|
||||
|
||||
export interface SubsystemConfig {
|
||||
bgColor: string
|
||||
color: string
|
||||
icon: string
|
||||
label: string
|
||||
subsystem: IOSubsystem
|
||||
label: string
|
||||
color: string
|
||||
bgColor: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
export const SUBSYSTEM_CONFIGS: SubsystemConfig[] = [
|
||||
{ bgColor: '#3b82f618', color: '#3b82f6', icon: 'i-lucide:mic', label: 'ASR', subsystem: IOSubsystems.ASR },
|
||||
{ bgColor: '#a855f718', color: '#a855f7', icon: 'i-lucide:brain', label: 'LLM', subsystem: IOSubsystems.LLM },
|
||||
{ bgColor: '#06b6d418', color: '#06b6d4', icon: 'i-lucide:radio-tower', label: 'Streaming Control', subsystem: IOSubsystems.StreamingControl },
|
||||
{ bgColor: '#22c55e18', color: '#22c55e', icon: 'i-lucide:audio-lines', label: 'TTS', subsystem: IOSubsystems.TTS },
|
||||
{ bgColor: '#f8717118', color: '#f87171', icon: 'i-lucide:play', label: 'Playback', subsystem: IOSubsystems.Playback },
|
||||
{ subsystem: IOSubsystems.ASR, label: 'ASR', color: '#3b82f6', bgColor: '#3b82f618', icon: 'i-lucide:mic' },
|
||||
{ subsystem: IOSubsystems.LLM, label: 'LLM', color: '#a855f7', bgColor: '#a855f718', icon: 'i-lucide:brain' },
|
||||
{ subsystem: IOSubsystems.StreamingControl, label: 'Streaming Control', color: '#06b6d4', bgColor: '#06b6d418', icon: 'i-lucide:radio-tower' },
|
||||
{ subsystem: IOSubsystems.TTS, label: 'TTS', color: '#22c55e', bgColor: '#22c55e18', icon: 'i-lucide:audio-lines' },
|
||||
{ subsystem: IOSubsystems.Playback, label: 'Playback', color: '#f87171', bgColor: '#f8717118', icon: 'i-lucide:play' },
|
||||
]
|
||||
|
||||
export const SUBSYSTEM_CONFIG_MAP = new Map(SUBSYSTEM_CONFIGS.map(c => [c.subsystem, c]))
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { shallowRef } from 'vue'
|
||||
|
||||
export type DataSettingsStatusEmit = (event: 'status', payload: DataSettingsStatusPayload) => void
|
||||
|
||||
export interface DataSettingsStatusEmits {
|
||||
status: [payload: DataSettingsStatusPayload]
|
||||
}
|
||||
export type DataSettingsStatusTone = 'neutral' | 'success' | 'error'
|
||||
|
||||
export interface DataSettingsStatusPayload {
|
||||
message: string
|
||||
tone: DataSettingsStatusTone
|
||||
}
|
||||
|
||||
export type DataSettingsStatusTone = 'error' | 'neutral' | 'success'
|
||||
export interface DataSettingsStatusEmits {
|
||||
status: [payload: DataSettingsStatusPayload]
|
||||
}
|
||||
|
||||
export type DataSettingsStatusEmit = (event: 'status', payload: DataSettingsStatusPayload) => void
|
||||
|
||||
export function createDataSettingsStatusHelpers(emit: DataSettingsStatusEmit) {
|
||||
function emitStatus(message: string, tone: DataSettingsStatusTone = 'success') {
|
||||
@@ -40,8 +40,8 @@ export function createDataSettingsStatusState() {
|
||||
}
|
||||
|
||||
return {
|
||||
handleStatus,
|
||||
statusMessage,
|
||||
statusTone,
|
||||
handleStatus,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user