feat(stage-tamagotchi,stage-ui,ui): input should not flink, added retry for errored item, adjusted button
This commit is contained in:
@@ -158,6 +158,13 @@ const historyMessages = computed(() => messages.value as unknown as ChatHistoryI
|
|||||||
async function handleDeleteMessage(index: number) {
|
async function handleDeleteMessage(index: number) {
|
||||||
await chatSyncStore.requestDeleteMessage({ index })
|
await chatSyncStore.requestDeleteMessage({ index })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleRetryMessage(index: number) {
|
||||||
|
await chatSyncStore.requestRetry({
|
||||||
|
sessionId: chatSession.activeSessionId,
|
||||||
|
index,
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -168,6 +175,7 @@ async function handleDeleteMessage(index: number) {
|
|||||||
:sending="sending"
|
:sending="sending"
|
||||||
:streaming-message="streamingMessage"
|
:streaming-message="streamingMessage"
|
||||||
@delete-message="handleDeleteMessage($event.index)"
|
@delete-message="handleDeleteMessage($event.index)"
|
||||||
|
@retry-message="handleRetryMessage($event.index)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -210,9 +218,13 @@ async function handleDeleteMessage(index: number) {
|
|||||||
side="top"
|
side="top"
|
||||||
:side-offset="8"
|
:side-offset="8"
|
||||||
:class="[
|
:class="[
|
||||||
'z-50 min-w-[180px] rounded-lg border border-primary-200 bg-white p-1 shadow-xl',
|
'z-50 min-w-[180px] rounded-xl p-1 shadow',
|
||||||
'dark:border-primary-700 dark:bg-neutral-800',
|
'bg-white dark:bg-neutral-800',
|
||||||
'flex flex-col gap-1',
|
'flex flex-col gap-1',
|
||||||
|
'data-[side=top]:animate-slideDownAndFade',
|
||||||
|
'data-[side=left]:animate-none',
|
||||||
|
'data-[side=bottom]:animate-none',
|
||||||
|
'data-[side=right]:animate-none',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -220,7 +232,7 @@ async function handleDeleteMessage(index: number) {
|
|||||||
:key="mode"
|
:key="mode"
|
||||||
:class="[
|
:class="[
|
||||||
'w-full flex cursor-pointer items-center rounded-md px-3 py-2 text-left text-xs outline-none transition-colors',
|
'w-full flex cursor-pointer items-center rounded-md px-3 py-2 text-left text-xs outline-none transition-colors',
|
||||||
'hover:bg-primary-100 dark:hover:bg-primary-900/50',
|
'hover:bg-primary-50 dark:hover:bg-primary-900/20',
|
||||||
sendMode === mode ? 'bg-primary-50 text-primary-600 font-semibold dark:bg-primary-900/20 dark:text-primary-300' : 'text-neutral-500',
|
sendMode === mode ? 'bg-primary-50 text-primary-600 font-semibold dark:bg-primary-900/20 dark:text-primary-300' : 'text-neutral-500',
|
||||||
]"
|
]"
|
||||||
@select="sendMode = mode"
|
@select="sendMode = mode"
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ interface MockState {
|
|||||||
activeSessionId: Ref<string>
|
activeSessionId: Ref<string>
|
||||||
sessionMessages: Ref<Record<string, Array<{ role: string, content: string }>>>
|
sessionMessages: Ref<Record<string, Array<{ role: string, content: string }>>>
|
||||||
sessionMetas: Ref<Record<string, unknown>>
|
sessionMetas: Ref<Record<string, unknown>>
|
||||||
|
applyRemoteSnapshot: ReturnType<typeof vi.fn>
|
||||||
setSessionMessages: ReturnType<typeof vi.fn>
|
setSessionMessages: ReturnType<typeof vi.fn>
|
||||||
getSessionMessages: ReturnType<typeof vi.fn>
|
getSessionMessages: ReturnType<typeof vi.fn>
|
||||||
ingest: ReturnType<typeof vi.fn>
|
ingest: ReturnType<typeof vi.fn>
|
||||||
@@ -78,7 +79,7 @@ vi.mock('@proj-airi/stage-ui/stores/chat/session-store', () => ({
|
|||||||
activeSessionId: mockState.activeSessionId,
|
activeSessionId: mockState.activeSessionId,
|
||||||
sessionMessages: mockState.sessionMessages,
|
sessionMessages: mockState.sessionMessages,
|
||||||
sessionMetas: mockState.sessionMetas,
|
sessionMetas: mockState.sessionMetas,
|
||||||
applyRemoteSnapshot: vi.fn(),
|
applyRemoteSnapshot: mockState.applyRemoteSnapshot,
|
||||||
getSnapshot: vi.fn(() => ({
|
getSnapshot: vi.fn(() => ({
|
||||||
activeSessionId: mockState.activeSessionId.value,
|
activeSessionId: mockState.activeSessionId.value,
|
||||||
sessionMessages: mockState.sessionMessages.value,
|
sessionMessages: mockState.sessionMessages.value,
|
||||||
@@ -147,6 +148,15 @@ describe('useChatSyncStore authority ingest failures', async () => {
|
|||||||
'session-1': [{ role: 'system', content: 'init' }],
|
'session-1': [{ role: 'system', content: 'init' }],
|
||||||
})
|
})
|
||||||
const sessionMetas = ref<Record<string, unknown>>({})
|
const sessionMetas = ref<Record<string, unknown>>({})
|
||||||
|
const applyRemoteSnapshot = vi.fn((snapshot: {
|
||||||
|
activeSessionId: string
|
||||||
|
sessionMessages: Record<string, Array<{ role: string, content: string }>>
|
||||||
|
sessionMetas: Record<string, unknown>
|
||||||
|
}) => {
|
||||||
|
activeSessionId.value = snapshot.activeSessionId
|
||||||
|
sessionMessages.value = snapshot.sessionMessages
|
||||||
|
sessionMetas.value = snapshot.sessionMetas
|
||||||
|
})
|
||||||
|
|
||||||
const setSessionMessages = vi.fn((sessionId: string, next: Array<{ role: string, content: string }>) => {
|
const setSessionMessages = vi.fn((sessionId: string, next: Array<{ role: string, content: string }>) => {
|
||||||
sessionMessages.value[sessionId] = next
|
sessionMessages.value[sessionId] = next
|
||||||
@@ -162,6 +172,7 @@ describe('useChatSyncStore authority ingest failures', async () => {
|
|||||||
activeSessionId,
|
activeSessionId,
|
||||||
sessionMessages,
|
sessionMessages,
|
||||||
sessionMetas,
|
sessionMetas,
|
||||||
|
applyRemoteSnapshot,
|
||||||
setSessionMessages,
|
setSessionMessages,
|
||||||
getSessionMessages,
|
getSessionMessages,
|
||||||
ingest,
|
ingest,
|
||||||
@@ -211,4 +222,148 @@ describe('useChatSyncStore authority ingest failures', async () => {
|
|||||||
peer.close()
|
peer.close()
|
||||||
store.dispose()
|
store.dispose()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @example
|
||||||
|
* it('replaces the last failed turn before retrying', async () => {
|
||||||
|
* // authority receives retry command for trailing user -> error pair
|
||||||
|
* // authoritative session removes that failed turn before re-ingesting the user text
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
it('replaces the last failed turn before retrying', async () => {
|
||||||
|
mockState.sessionMessages.value['session-1'] = [
|
||||||
|
{ role: 'system', content: 'init' },
|
||||||
|
{ role: 'user', content: 'hello-1' },
|
||||||
|
{ role: 'assistant', content: 'answer-1' },
|
||||||
|
{ role: 'user', content: 'hello' },
|
||||||
|
{ role: 'error', content: 'Remote sent 400 response' },
|
||||||
|
{ role: 'user', content: 'hello-3' },
|
||||||
|
{ role: 'assistant', content: 'answer-3' },
|
||||||
|
]
|
||||||
|
mockState.ingest.mockResolvedValueOnce(undefined)
|
||||||
|
|
||||||
|
const store = useChatSyncStore()
|
||||||
|
store.initialize('authority')
|
||||||
|
|
||||||
|
const peer = new MockBroadcastChannel('airi:stage-tamagotchi:chat-sync')
|
||||||
|
peer.postMessage({
|
||||||
|
type: 'command',
|
||||||
|
requestId: 'req-2',
|
||||||
|
senderId: 'peer',
|
||||||
|
command: 'retry',
|
||||||
|
payload: {
|
||||||
|
sessionId: 'session-1',
|
||||||
|
index: 4,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(mockState.setSessionMessages).toHaveBeenCalledWith('session-1', [
|
||||||
|
{ role: 'system', content: 'init' },
|
||||||
|
{ role: 'user', content: 'hello-1' },
|
||||||
|
{ role: 'assistant', content: 'answer-1' },
|
||||||
|
])
|
||||||
|
expect(mockState.ingest).toHaveBeenCalledWith('hello', expect.any(Object), 'session-1')
|
||||||
|
})
|
||||||
|
|
||||||
|
const persistedMessages = mockState.sessionMessages.value['session-1']
|
||||||
|
expect(persistedMessages).toEqual([
|
||||||
|
{ role: 'system', content: 'init' },
|
||||||
|
{ role: 'user', content: 'hello-1' },
|
||||||
|
{ role: 'assistant', content: 'answer-1' },
|
||||||
|
])
|
||||||
|
|
||||||
|
peer.close()
|
||||||
|
store.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @example
|
||||||
|
* it('rewinds from the source user turn when retry targets an assistant message', async () => {
|
||||||
|
* // future assistant retry still trims the whole tail from its originating user turn
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
it('rewinds from the source user turn when retry targets an assistant message', async () => {
|
||||||
|
mockState.sessionMessages.value['session-1'] = [
|
||||||
|
{ role: 'system', content: 'init' },
|
||||||
|
{ role: 'user', content: 'hello-1' },
|
||||||
|
{ role: 'assistant', content: 'answer-1' },
|
||||||
|
{ role: 'user', content: 'hello-2' },
|
||||||
|
{ role: 'assistant', content: 'answer-2' },
|
||||||
|
{ role: 'user', content: 'hello-3' },
|
||||||
|
]
|
||||||
|
mockState.ingest.mockResolvedValueOnce(undefined)
|
||||||
|
|
||||||
|
const store = useChatSyncStore()
|
||||||
|
store.initialize('authority')
|
||||||
|
|
||||||
|
const peer = new MockBroadcastChannel('airi:stage-tamagotchi:chat-sync')
|
||||||
|
peer.postMessage({
|
||||||
|
type: 'command',
|
||||||
|
requestId: 'req-3',
|
||||||
|
senderId: 'peer',
|
||||||
|
command: 'retry',
|
||||||
|
payload: {
|
||||||
|
sessionId: 'session-1',
|
||||||
|
index: 4,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(mockState.setSessionMessages).toHaveBeenCalledWith('session-1', [
|
||||||
|
{ role: 'system', content: 'init' },
|
||||||
|
{ role: 'user', content: 'hello-1' },
|
||||||
|
{ role: 'assistant', content: 'answer-1' },
|
||||||
|
])
|
||||||
|
expect(mockState.ingest).toHaveBeenCalledWith('hello-2', expect.any(Object), 'session-1')
|
||||||
|
})
|
||||||
|
|
||||||
|
peer.close()
|
||||||
|
store.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @example
|
||||||
|
* it('keeps the follower chat window on its local session while applying remote snapshots', async () => {
|
||||||
|
* // follower already displays session-2
|
||||||
|
* // authority snapshot arrives with session-1 as active
|
||||||
|
* // follower keeps session-2 selected but still receives session-2 message updates
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
it('keeps the follower chat window on its local session while applying remote snapshots', async () => {
|
||||||
|
mockState.activeSessionId.value = 'session-2'
|
||||||
|
mockState.sessionMessages.value = {
|
||||||
|
'session-2': [{ role: 'system', content: 'chat-window' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = useChatSyncStore()
|
||||||
|
store.initialize('follower')
|
||||||
|
|
||||||
|
const authority = new MockBroadcastChannel('airi:stage-tamagotchi:chat-sync')
|
||||||
|
authority.postMessage({
|
||||||
|
type: 'session-snapshot',
|
||||||
|
authorityId: 'authority',
|
||||||
|
snapshot: {
|
||||||
|
activeSessionId: 'session-1',
|
||||||
|
sessionMessages: {
|
||||||
|
'session-1': [{ role: 'system', content: 'main-window' }],
|
||||||
|
'session-2': [{ role: 'system', content: 'chat-window' }, { role: 'user', content: 'retry me' }],
|
||||||
|
},
|
||||||
|
sessionMetas: {},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(mockState.applyRemoteSnapshot).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockState.activeSessionId.value).toBe('session-2')
|
||||||
|
expect(mockState.sessionMessages.value['session-2']).toEqual([
|
||||||
|
{ role: 'system', content: 'chat-window' },
|
||||||
|
{ role: 'user', content: 'retry me' },
|
||||||
|
])
|
||||||
|
|
||||||
|
authority.close()
|
||||||
|
store.dispose()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -44,12 +44,18 @@ interface IngestCommandPayload {
|
|||||||
toolset?: ToolsetId
|
toolset?: ToolsetId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RetryCommandPayload {
|
||||||
|
sessionId?: string
|
||||||
|
index: number
|
||||||
|
}
|
||||||
|
|
||||||
type ChatSyncMessage
|
type ChatSyncMessage
|
||||||
= | { type: 'authority-announcement', authorityId: string, sentAt: number }
|
= | { type: 'authority-announcement', authorityId: string, sentAt: number }
|
||||||
| { type: 'request-snapshot', requestId: string, senderId: string }
|
| { type: 'request-snapshot', requestId: string, senderId: string }
|
||||||
| { type: 'session-snapshot', authorityId: string, snapshot: SessionSnapshotPayload }
|
| { type: 'session-snapshot', authorityId: string, snapshot: SessionSnapshotPayload }
|
||||||
| { type: 'stream-snapshot', authorityId: string, snapshot: StreamSnapshotPayload }
|
| { type: 'stream-snapshot', authorityId: string, snapshot: StreamSnapshotPayload }
|
||||||
| { type: 'command', authorityId?: string, requestId: string, senderId: string, command: 'ingest', payload: IngestCommandPayload }
|
| { type: 'command', authorityId?: string, requestId: string, senderId: string, command: 'ingest', payload: IngestCommandPayload }
|
||||||
|
| { type: 'command', authorityId?: string, requestId: string, senderId: string, command: 'retry', payload: RetryCommandPayload }
|
||||||
| { type: 'command', authorityId?: string, requestId: string, senderId: string, command: 'cleanup', payload: { sessionId?: string } }
|
| { type: 'command', authorityId?: string, requestId: string, senderId: string, command: 'cleanup', payload: { sessionId?: string } }
|
||||||
| { type: 'command', authorityId?: string, requestId: string, senderId: string, command: 'delete-message', payload: { sessionId?: string, messageId?: string, index?: number } }
|
| { type: 'command', authorityId?: string, requestId: string, senderId: string, command: 'delete-message', payload: { sessionId?: string, messageId?: string, index?: number } }
|
||||||
| { type: 'response', requestId: string, authorityId: string, ok: boolean, error?: string }
|
| { type: 'response', requestId: string, authorityId: string, ok: boolean, error?: string }
|
||||||
@@ -68,6 +74,50 @@ function createRequestId() {
|
|||||||
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRetryText(message: ChatHistoryItem | undefined): string | null {
|
||||||
|
if (!message || message.role !== 'user')
|
||||||
|
return null
|
||||||
|
|
||||||
|
if (typeof message.content === 'string') {
|
||||||
|
const text = message.content.trim()
|
||||||
|
return text || null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(message.content))
|
||||||
|
return null
|
||||||
|
|
||||||
|
const text = message.content.reduce<string[]>((texts, part) => {
|
||||||
|
if (part.type !== 'text')
|
||||||
|
return texts
|
||||||
|
|
||||||
|
const value = part.text?.trim()
|
||||||
|
if (value)
|
||||||
|
texts.push(value)
|
||||||
|
|
||||||
|
return texts
|
||||||
|
}, []).join('\n\n')
|
||||||
|
|
||||||
|
return text || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRetrySourceIndex(messages: ChatHistoryItem[], index: number): number {
|
||||||
|
const targetMessage = messages[index]
|
||||||
|
if (!targetMessage)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
if (targetMessage.role === 'user')
|
||||||
|
return index
|
||||||
|
|
||||||
|
if (targetMessage.role === 'assistant' || targetMessage.role === 'error') {
|
||||||
|
for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
|
||||||
|
if (messages[cursor]?.role === 'user')
|
||||||
|
return cursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
export const useChatSyncStore = defineStore('stage-tamagotchi:chat-sync', () => {
|
export const useChatSyncStore = defineStore('stage-tamagotchi:chat-sync', () => {
|
||||||
const instanceId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`
|
const instanceId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`
|
||||||
const mode = ref<ChatSyncMode>('inactive')
|
const mode = ref<ChatSyncMode>('inactive')
|
||||||
@@ -169,7 +219,17 @@ export const useChatSyncStore = defineStore('stage-tamagotchi:chat-sync', () =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applySessionSnapshot(snapshot: SessionSnapshotPayload) {
|
function applySessionSnapshot(snapshot: SessionSnapshotPayload) {
|
||||||
chatSession.applyRemoteSnapshot(snapshot)
|
const localActiveSessionId = activeSessionId.value
|
||||||
|
const shouldPreserveLocalActiveSession = mode.value === 'follower'
|
||||||
|
&& !!localActiveSessionId
|
||||||
|
&& !!snapshot.sessionMessages[localActiveSessionId]
|
||||||
|
|
||||||
|
chatSession.applyRemoteSnapshot({
|
||||||
|
...snapshot,
|
||||||
|
activeSessionId: shouldPreserveLocalActiveSession
|
||||||
|
? localActiveSessionId
|
||||||
|
: snapshot.activeSessionId,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyStreamSnapshot(snapshot: StreamSnapshotPayload) {
|
function applyStreamSnapshot(snapshot: StreamSnapshotPayload) {
|
||||||
@@ -216,6 +276,27 @@ export const useChatSyncStore = defineStore('stage-tamagotchi:chat-sync', () =>
|
|||||||
}, payload.sessionId)
|
}, payload.sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function executeRetry(payload: RetryCommandPayload) {
|
||||||
|
const sessionId = payload.sessionId || chatSession.activeSessionId
|
||||||
|
const currentMessages = chatSession.getSessionMessages(sessionId)
|
||||||
|
const sourceIndex = resolveRetrySourceIndex(currentMessages, payload.index)
|
||||||
|
if (sourceIndex < 0)
|
||||||
|
throw new Error('Retry target has no retriable source message')
|
||||||
|
|
||||||
|
const text = getRetryText(currentMessages[sourceIndex])
|
||||||
|
if (!text)
|
||||||
|
throw new Error('Retry target has no retriable user message')
|
||||||
|
|
||||||
|
const nextMessages = currentMessages.slice(0, sourceIndex)
|
||||||
|
chatSession.setSessionMessages(sessionId, nextMessages)
|
||||||
|
|
||||||
|
await executeIngest({
|
||||||
|
text,
|
||||||
|
sessionId,
|
||||||
|
toolset: 'widgets',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function executeDeleteMessage(payload: { sessionId?: string, messageId?: string, index?: number }) {
|
function executeDeleteMessage(payload: { sessionId?: string, messageId?: string, index?: number }) {
|
||||||
const sessionId = payload.sessionId || chatSession.activeSessionId
|
const sessionId = payload.sessionId || chatSession.activeSessionId
|
||||||
const nextMessages = chatSession.getSessionMessages(sessionId).filter((message, index) => {
|
const nextMessages = chatSession.getSessionMessages(sessionId).filter((message, index) => {
|
||||||
@@ -260,6 +341,9 @@ export const useChatSyncStore = defineStore('stage-tamagotchi:chat-sync', () =>
|
|||||||
case 'ingest':
|
case 'ingest':
|
||||||
await executeIngest(message.payload)
|
await executeIngest(message.payload)
|
||||||
break
|
break
|
||||||
|
case 'retry':
|
||||||
|
await executeRetry(message.payload)
|
||||||
|
break
|
||||||
case 'cleanup':
|
case 'cleanup':
|
||||||
cleanupMessages(message.payload.sessionId)
|
cleanupMessages(message.payload.sessionId)
|
||||||
break
|
break
|
||||||
@@ -402,6 +486,21 @@ export const useChatSyncStore = defineStore('stage-tamagotchi:chat-sync', () =>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function requestRetry(payload: RetryCommandPayload) {
|
||||||
|
if (mode.value === 'authority') {
|
||||||
|
await executeRetry(payload)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return await dispatchCommand({
|
||||||
|
type: 'command',
|
||||||
|
requestId: createRequestId(),
|
||||||
|
senderId: instanceId,
|
||||||
|
command: 'retry',
|
||||||
|
payload,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function requestCleanup(sessionId?: string) {
|
async function requestCleanup(sessionId?: string) {
|
||||||
if (mode.value === 'authority') {
|
if (mode.value === 'authority') {
|
||||||
cleanupMessages(sessionId)
|
cleanupMessages(sessionId)
|
||||||
@@ -447,6 +546,7 @@ export const useChatSyncStore = defineStore('stage-tamagotchi:chat-sync', () =>
|
|||||||
initialize,
|
initialize,
|
||||||
dispose,
|
dispose,
|
||||||
requestIngest,
|
requestIngest,
|
||||||
|
requestRetry,
|
||||||
requestCleanup,
|
requestCleanup,
|
||||||
requestDeleteMessage,
|
requestDeleteMessage,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
"@unocss/eslint-plugin": "^66.6.8",
|
"@unocss/eslint-plugin": "^66.6.8",
|
||||||
"@unocss/preset-mini": "^66.6.8",
|
"@unocss/preset-mini": "^66.6.8",
|
||||||
"@unocss/preset-web-fonts": "^66.6.8",
|
"@unocss/preset-web-fonts": "^66.6.8",
|
||||||
|
"@vitest/browser-playwright": "catalog:vitest",
|
||||||
"@vitest/coverage-v8": "catalog:vitest",
|
"@vitest/coverage-v8": "catalog:vitest",
|
||||||
"bumpp": "^11.0.1",
|
"bumpp": "^11.0.1",
|
||||||
"eslint": "^10.2.1",
|
"eslint": "^10.2.1",
|
||||||
@@ -85,6 +86,7 @@
|
|||||||
"vite": "^8.0.8",
|
"vite": "^8.0.8",
|
||||||
"vite-plugin-inspect": "catalog:",
|
"vite-plugin-inspect": "catalog:",
|
||||||
"vitest": "^4.1.4",
|
"vitest": "^4.1.4",
|
||||||
|
"vitest-browser-vue": "catalog:",
|
||||||
"yaml": "^2.8.3"
|
"yaml": "^2.8.3"
|
||||||
},
|
},
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
|
|||||||
@@ -10,9 +10,5 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@moeru/std": "catalog:",
|
"@moeru/std": "catalog:",
|
||||||
"uncrypto": "catalog:"
|
"uncrypto": "catalog:"
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@vitest/browser-playwright": "catalog:vitest",
|
|
||||||
"vitest": "catalog:vitest"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
chat:
|
chat:
|
||||||
|
actions:
|
||||||
|
retry: Retry
|
||||||
message:
|
message:
|
||||||
character-name:
|
character-name:
|
||||||
airi: AIRI
|
airi: AIRI
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
chat:
|
chat:
|
||||||
|
actions:
|
||||||
|
retry: Volver a intentar
|
||||||
message:
|
message:
|
||||||
character-name:
|
character-name:
|
||||||
airi: AIRI
|
airi: AIRI
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
chat:
|
chat:
|
||||||
|
actions:
|
||||||
|
retry: Réessayer
|
||||||
message:
|
message:
|
||||||
character-name:
|
character-name:
|
||||||
airi: AIRI
|
airi: AIRI
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
chat:
|
chat:
|
||||||
|
actions:
|
||||||
|
retry: 再試行
|
||||||
message:
|
message:
|
||||||
character-name:
|
character-name:
|
||||||
airi: AIRI
|
airi: AIRI
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
chat:
|
chat:
|
||||||
|
actions:
|
||||||
|
retry: 다시 시도
|
||||||
message:
|
message:
|
||||||
character-name:
|
character-name:
|
||||||
airi: AIRI
|
airi: AIRI
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
chat:
|
chat:
|
||||||
|
actions:
|
||||||
|
retry: Повторить
|
||||||
message:
|
message:
|
||||||
character-name:
|
character-name:
|
||||||
airi: AIRI
|
airi: AIRI
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
chat:
|
chat:
|
||||||
|
actions:
|
||||||
|
retry: Thử lại
|
||||||
message:
|
message:
|
||||||
character-name:
|
character-name:
|
||||||
airi: AIRI
|
airi: AIRI
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
chat:
|
chat:
|
||||||
|
actions:
|
||||||
|
retry: 重试
|
||||||
message:
|
message:
|
||||||
character-name:
|
character-name:
|
||||||
airi: AIRI
|
airi: AIRI
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
chat:
|
chat:
|
||||||
|
actions:
|
||||||
|
retry: 重試
|
||||||
message:
|
message:
|
||||||
character-name:
|
character-name:
|
||||||
airi: AIRI
|
airi: AIRI
|
||||||
|
|||||||
@@ -195,6 +195,7 @@
|
|||||||
"unplugin-info": "catalog:",
|
"unplugin-info": "catalog:",
|
||||||
"unplugin-yaml": "^4.1.0",
|
"unplugin-yaml": "^4.1.0",
|
||||||
"vite": "^6.4.2",
|
"vite": "^6.4.2",
|
||||||
|
"vitest-browser-vue": "catalog:",
|
||||||
"vue": "catalog:",
|
"vue": "catalog:",
|
||||||
"vue-tsc": "^3.2.6"
|
"vue-tsc": "^3.2.6"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import { createChatActionMenuItems } from './menu-items'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @example
|
||||||
|
* describe('createChatActionMenuItems', () => {
|
||||||
|
* it('includes retry between copy and delete when retry is available', () => {})
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
describe('createChatActionMenuItems', () => {
|
||||||
|
/**
|
||||||
|
* @example
|
||||||
|
* it('includes retry between copy and delete when retry is available', () => {
|
||||||
|
* const items = createChatActionMenuItems({ canCopy: true, canRetry: true, canDelete: true })
|
||||||
|
* expect(items.map(item => item.action)).toEqual(['copy', 'retry', 'delete'])
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
it('includes retry between copy and delete when retry is available', () => {
|
||||||
|
const items = createChatActionMenuItems({
|
||||||
|
canCopy: true,
|
||||||
|
canRetry: true,
|
||||||
|
canDelete: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(items.map(item => item.action)).toEqual(['copy', 'retry', 'delete'])
|
||||||
|
expect(items[1]?.label).toBe('Retry')
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @example
|
||||||
|
* it('omits retry when retry is unavailable', () => {
|
||||||
|
* const items = createChatActionMenuItems({ canCopy: true, canRetry: false, canDelete: true })
|
||||||
|
* expect(items.map(item => item.action)).toEqual(['copy', 'delete'])
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
it('omits retry when retry is unavailable', () => {
|
||||||
|
const items = createChatActionMenuItems({
|
||||||
|
canCopy: true,
|
||||||
|
canRetry: false,
|
||||||
|
canDelete: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(items.map(item => item.action)).toEqual(['copy', 'delete'])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,33 +1,3 @@
|
|||||||
export type ChatActionMenuAction = 'copy' | 'delete'
|
|
||||||
|
|
||||||
export interface ChatActionMenuItem {
|
|
||||||
action: ChatActionMenuAction
|
|
||||||
label: string
|
|
||||||
icon: string
|
|
||||||
danger?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createChatActionMenuItems(options: {
|
|
||||||
canCopy: boolean
|
|
||||||
canDelete: boolean
|
|
||||||
}): ChatActionMenuItem[] {
|
|
||||||
return [
|
|
||||||
options.canCopy
|
|
||||||
? {
|
|
||||||
action: 'copy',
|
|
||||||
label: 'Copy',
|
|
||||||
icon: 'i-solar:copy-bold',
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
options.canDelete
|
|
||||||
? {
|
|
||||||
action: 'delete',
|
|
||||||
label: 'Delete',
|
|
||||||
icon: 'i-solar:trash-bin-minimalistic-bold',
|
|
||||||
danger: true,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
].filter(Boolean) as ChatActionMenuItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export { default as ChatActionMenu } from './index.vue'
|
export { default as ChatActionMenu } from './index.vue'
|
||||||
|
export type { ChatActionMenuAction, ChatActionMenuItem } from './menu-items'
|
||||||
|
export { createChatActionMenuItems } from './menu-items'
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from 'reka-ui'
|
} from 'reka-ui'
|
||||||
import { computed, inject, reactive, ref, shallowRef, toRef, useTemplateRef, watch } from 'vue'
|
import { computed, inject, reactive, ref, shallowRef, toRef, useTemplateRef, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useWebHaptics } from 'web-haptics/vue'
|
import { useWebHaptics } from 'web-haptics/vue'
|
||||||
|
|
||||||
import { createChatActionMenuItems } from '.'
|
import { createChatActionMenuItems } from '.'
|
||||||
@@ -30,12 +31,14 @@ import { chatScrollContainerKey } from '../../constants'
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
canCopy?: boolean
|
canCopy?: boolean
|
||||||
|
canRetry?: boolean
|
||||||
canDelete?: boolean
|
canDelete?: boolean
|
||||||
copyText?: string
|
copyText?: string
|
||||||
menuLabel?: string
|
menuLabel?: string
|
||||||
placement?: 'left' | 'right'
|
placement?: 'left' | 'right'
|
||||||
}>(), {
|
}>(), {
|
||||||
canCopy: true,
|
canCopy: true,
|
||||||
|
canRetry: false,
|
||||||
canDelete: true,
|
canDelete: true,
|
||||||
copyText: '',
|
copyText: '',
|
||||||
menuLabel: 'Message actions',
|
menuLabel: 'Message actions',
|
||||||
@@ -44,6 +47,7 @@ const props = withDefaults(defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'copy'): void
|
(e: 'copy'): void
|
||||||
|
(e: 'retry'): void
|
||||||
(e: 'delete'): void
|
(e: 'delete'): void
|
||||||
}>()
|
}>()
|
||||||
defineSlots<{
|
defineSlots<{
|
||||||
@@ -79,11 +83,14 @@ const bottomSentinelVisible = useElementVisibility(bottomSentinelRef, {
|
|||||||
|
|
||||||
const { trigger } = useWebHaptics()
|
const { trigger } = useWebHaptics()
|
||||||
const { isMobile } = useBreakpoints()
|
const { isMobile } = useBreakpoints()
|
||||||
|
const { t } = useI18n()
|
||||||
const shouldDisableDropdownMenu = computed(() => (isStageWeb() || isStageCapacitor()) && isMobile.value)
|
const shouldDisableDropdownMenu = computed(() => (isStageWeb() || isStageCapacitor()) && isMobile.value)
|
||||||
|
|
||||||
const menuItems = computed(() => createChatActionMenuItems({
|
const menuItems = computed(() => createChatActionMenuItems({
|
||||||
canCopy: props.canCopy && props.copyText.trim().length > 0,
|
canCopy: props.canCopy && props.copyText.trim().length > 0,
|
||||||
|
canRetry: props.canRetry,
|
||||||
canDelete: props.canDelete,
|
canDelete: props.canDelete,
|
||||||
|
retryLabel: t('stage.chat.actions.retry'),
|
||||||
}))
|
}))
|
||||||
const hasMenuItems = computed(() => menuItems.value.length > 0)
|
const hasMenuItems = computed(() => menuItems.value.length > 0)
|
||||||
const forceVisible = computed(() => contextMenuOpen.value)
|
const forceVisible = computed(() => contextMenuOpen.value)
|
||||||
@@ -143,6 +150,11 @@ async function handleAction(action: ChatActionMenuAction) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action === 'retry') {
|
||||||
|
emit('retry')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
emit('delete')
|
emit('delete')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
export type ChatActionMenuAction = 'copy' | 'retry' | 'delete'
|
||||||
|
|
||||||
|
export interface ChatActionMenuItem {
|
||||||
|
action: ChatActionMenuAction
|
||||||
|
label: string
|
||||||
|
icon: string
|
||||||
|
danger?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createChatActionMenuItems(options: {
|
||||||
|
canCopy: boolean
|
||||||
|
canRetry: boolean
|
||||||
|
canDelete: boolean
|
||||||
|
retryLabel?: string
|
||||||
|
}): ChatActionMenuItem[] {
|
||||||
|
return [
|
||||||
|
options.canCopy
|
||||||
|
? {
|
||||||
|
action: 'copy',
|
||||||
|
label: 'Copy',
|
||||||
|
icon: 'i-solar:copy-bold',
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
options.canRetry
|
||||||
|
? {
|
||||||
|
action: 'retry',
|
||||||
|
label: options.retryLabel ?? 'Retry',
|
||||||
|
icon: 'i-solar:refresh-bold',
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
options.canDelete
|
||||||
|
? {
|
||||||
|
action: 'delete',
|
||||||
|
label: 'Delete',
|
||||||
|
icon: 'i-solar:trash-bin-minimalistic-bold',
|
||||||
|
danger: true,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
].filter(Boolean) as ChatActionMenuItem[]
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import type { ChatHistoryItem, ErrorMessage } from '../../../../types/chat'
|
import type { ChatHistoryItem, ErrorMessage } from '../../../../types/chat'
|
||||||
|
|
||||||
import { isStageCapacitor, isStageWeb } from '@proj-airi/stage-shared'
|
import { isStageCapacitor, isStageWeb } from '@proj-airi/stage-shared'
|
||||||
|
import { Button } from '@proj-airi/ui'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import { MarkdownRenderer } from '../../../markdown'
|
import { MarkdownRenderer } from '../../../markdown'
|
||||||
@@ -11,15 +12,19 @@ import { ChatActionMenu } from './action-menu'
|
|||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
message: ErrorMessage
|
message: ErrorMessage
|
||||||
label: string
|
label: string
|
||||||
|
retryLabel?: string
|
||||||
|
canRetry?: boolean
|
||||||
showPlaceholder?: boolean
|
showPlaceholder?: boolean
|
||||||
variant?: 'desktop' | 'mobile'
|
variant?: 'desktop' | 'mobile'
|
||||||
}>(), {
|
}>(), {
|
||||||
|
canRetry: false,
|
||||||
showPlaceholder: false,
|
showPlaceholder: false,
|
||||||
variant: 'desktop',
|
variant: 'desktop',
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'copy'): void
|
(e: 'copy'): void
|
||||||
|
(e: 'retry'): void
|
||||||
(e: 'delete'): void
|
(e: 'delete'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -32,20 +37,30 @@ const copyText = computed(() => getChatHistoryItemCopyText(props.message as Chat
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div flex :class="variant === 'mobile' ? 'mr-0' : 'mr-12'">
|
<div
|
||||||
|
:class="[
|
||||||
|
'flex flex-col',
|
||||||
|
variant === 'mobile' ? 'mr-0' : 'mr-12',
|
||||||
|
]"
|
||||||
|
>
|
||||||
<ChatActionMenu
|
<ChatActionMenu
|
||||||
:copy-text="copyText"
|
:copy-text="copyText"
|
||||||
:can-delete="!showPlaceholder"
|
:can-delete="!showPlaceholder"
|
||||||
|
:can-retry="canRetry && !showPlaceholder"
|
||||||
@copy="emit('copy')"
|
@copy="emit('copy')"
|
||||||
|
@retry="emit('retry')"
|
||||||
@delete="emit('delete')"
|
@delete="emit('delete')"
|
||||||
>
|
>
|
||||||
<template #default="{ setMeasuredElement }">
|
<template #default="{ setMeasuredElement }">
|
||||||
<div
|
<div
|
||||||
:ref="setMeasuredElement"
|
:ref="setMeasuredElement"
|
||||||
flex="~ col" shadow="sm violet-200/50 dark:none"
|
|
||||||
min-w-20 rounded-xl h="unset <sm:fit"
|
|
||||||
:class="[
|
:class="[
|
||||||
boxClasses,
|
boxClasses,
|
||||||
|
'relative',
|
||||||
|
'flex flex-col',
|
||||||
|
'min-w-20 rounded-xl',
|
||||||
|
'h-unset <sm:h-fit',
|
||||||
|
'shadow-sm shadow-violet-200/50 dark:shadow-none',
|
||||||
'bg-violet-100/80 dark:bg-violet-950/80',
|
'bg-violet-100/80 dark:bg-violet-950/80',
|
||||||
(isStageWeb() || isStageCapacitor()) && props.variant === 'mobile' ? 'select-none sm:select-auto' : '',
|
(isStageWeb() || isStageCapacitor()) && props.variant === 'mobile' ? 'select-none sm:select-auto' : '',
|
||||||
]"
|
]"
|
||||||
@@ -65,5 +80,19 @@ const copyText = computed(() => getChatHistoryItemCopyText(props.message as Chat
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ChatActionMenu>
|
</ChatActionMenu>
|
||||||
|
<div
|
||||||
|
v-if="canRetry && !showPlaceholder"
|
||||||
|
:class="[
|
||||||
|
'self-end mt-1',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
shape="square"
|
||||||
|
icon="i-solar:refresh-bold"
|
||||||
|
@click="emit('retry')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import type { ChatHistoryItem } from '../../../../types/chat'
|
||||||
|
|
||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
import { render } from 'vitest-browser-vue'
|
||||||
|
import { defineComponent, shallowRef } from 'vue'
|
||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import ChatHistory from './history.vue'
|
||||||
|
|
||||||
|
vi.mock('../composables/use-chat-history-scroll', () => ({
|
||||||
|
useChatHistoryScroll: () => undefined,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../../../markdown', () => ({
|
||||||
|
MarkdownRenderer: defineComponent({
|
||||||
|
name: 'MarkdownRendererStub',
|
||||||
|
props: {
|
||||||
|
content: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: '<div>{{ content }}</div>',
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
function createTestI18n() {
|
||||||
|
return createI18n({
|
||||||
|
legacy: false,
|
||||||
|
locale: 'en',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
stage: {
|
||||||
|
chat: {
|
||||||
|
actions: {
|
||||||
|
retry: 'Retry',
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
'character-name': {
|
||||||
|
'airi': 'AIRI',
|
||||||
|
'core-system': 'System',
|
||||||
|
'you': 'You',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function createHarness(messages: ChatHistoryItem[]) {
|
||||||
|
return defineComponent({
|
||||||
|
name: 'ChatHistoryRetryHarness',
|
||||||
|
components: {
|
||||||
|
ChatHistory,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const lastRetryIndex = shallowRef('none')
|
||||||
|
|
||||||
|
function handleRetryMessage(payload: { index: number }) {
|
||||||
|
lastRetryIndex.value = String(payload.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleRetryMessage,
|
||||||
|
lastRetryIndex,
|
||||||
|
messages,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<ChatHistory
|
||||||
|
:messages="messages"
|
||||||
|
@retry-message="handleRetryMessage"
|
||||||
|
/>
|
||||||
|
<output aria-label="retry-index">{{ lastRetryIndex }}</output>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @example
|
||||||
|
* describe('ChatHistory retry actions', () => {
|
||||||
|
* it('emits retry-message when the retry button is clicked for an error after a user message', async () => {})
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
describe('chatHistory retry actions', () => {
|
||||||
|
/**
|
||||||
|
* @example
|
||||||
|
* it('emits retry-message when the retry button is clicked for an error after a user message', async () => {
|
||||||
|
* const screen = await render(createHarness(messages), { global: { plugins: [createTestI18n()] } })
|
||||||
|
* await screen.getByRole('button', { name: 'Retry' }).click()
|
||||||
|
* await expect.element(screen.getByLabelText('retry-index')).toHaveTextContent('1')
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
it('emits retry-message when the retry button is clicked for an error after a user message', async () => {
|
||||||
|
const messages: ChatHistoryItem[] = [
|
||||||
|
{ role: 'user', content: 'hello' },
|
||||||
|
{ role: 'error', content: 'Remote sent 400 response' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const screen = await render(createHarness(messages), {
|
||||||
|
global: {
|
||||||
|
plugins: [createTestI18n()],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await screen.getByRole('button', { name: 'Retry' }).click()
|
||||||
|
|
||||||
|
await expect.element(screen.getByLabelText('retry-index')).toHaveTextContent('1')
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @example
|
||||||
|
* it('does not render the retry button when the error is not preceded by a user message', async () => {
|
||||||
|
* const screen = await render(createHarness(messages), { global: { plugins: [createTestI18n()] } })
|
||||||
|
* expect(document.body.textContent).not.toContain('Retry')
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
it('does not render the retry button when the error is not preceded by a user message', async () => {
|
||||||
|
const messages: ChatHistoryItem[] = [
|
||||||
|
{ role: 'assistant', content: 'hello', slices: [], tool_results: [] },
|
||||||
|
{ role: 'error', content: 'Remote sent 400 response' },
|
||||||
|
]
|
||||||
|
|
||||||
|
await render(createHarness(messages), {
|
||||||
|
global: {
|
||||||
|
plugins: [createTestI18n()],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(document.body.textContent).not.toContain('Retry')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -19,6 +19,7 @@ const props = withDefaults(defineProps<{
|
|||||||
assistantLabel?: string
|
assistantLabel?: string
|
||||||
userLabel?: string
|
userLabel?: string
|
||||||
errorLabel?: string
|
errorLabel?: string
|
||||||
|
retryLabel?: string
|
||||||
variant?: 'desktop' | 'mobile'
|
variant?: 'desktop' | 'mobile'
|
||||||
}>(), {
|
}>(), {
|
||||||
sending: false,
|
sending: false,
|
||||||
@@ -28,6 +29,7 @@ const props = withDefaults(defineProps<{
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'copyMessage', payload: { message: ChatHistoryItem, index: number, key: string | number }): void
|
(e: 'copyMessage', payload: { message: ChatHistoryItem, index: number, key: string | number }): void
|
||||||
(e: 'deleteMessage', payload: { message: ChatHistoryItem, index: number, key: string | number }): void
|
(e: 'deleteMessage', payload: { message: ChatHistoryItem, index: number, key: string | number }): void
|
||||||
|
(e: 'retryMessage', payload: { message: ChatHistoryItem, index: number, key: string | number }): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const chatHistoryRef = ref<HTMLDivElement>()
|
const chatHistoryRef = ref<HTMLDivElement>()
|
||||||
@@ -38,6 +40,7 @@ const labels = computed(() => ({
|
|||||||
assistant: props.assistantLabel ?? t('stage.chat.message.character-name.airi'),
|
assistant: props.assistantLabel ?? t('stage.chat.message.character-name.airi'),
|
||||||
user: props.userLabel ?? t('stage.chat.message.character-name.you'),
|
user: props.userLabel ?? t('stage.chat.message.character-name.you'),
|
||||||
error: props.errorLabel ?? t('stage.chat.message.character-name.core-system'),
|
error: props.errorLabel ?? t('stage.chat.message.character-name.core-system'),
|
||||||
|
retry: props.retryLabel ?? t('stage.chat.actions.retry'),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const streaming = computed<ChatAssistantMessage & { context?: ContextMessage } & { createdAt?: number }>(() => props.streamingMessage ?? { role: 'assistant', content: '', slices: [], tool_results: [], createdAt: Date.now() })
|
const streaming = computed<ChatAssistantMessage & { context?: ContextMessage } & { createdAt?: number }>(() => props.streamingMessage ?? { role: 'assistant', content: '', slices: [], tool_results: [], createdAt: Date.now() })
|
||||||
@@ -86,6 +89,14 @@ function emitDeleteMessage(message: ChatHistoryItem, index: number) {
|
|||||||
key: getChatHistoryItemKey(message, index),
|
key: getChatHistoryItemKey(message, index),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function emitRetryMessage(message: ChatHistoryItem, index: number) {
|
||||||
|
emit('retryMessage', {
|
||||||
|
message,
|
||||||
|
index,
|
||||||
|
key: getChatHistoryItemKey(message, index),
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -100,9 +111,12 @@ function emitDeleteMessage(message: ChatHistoryItem, index: number) {
|
|||||||
v-if="message.role === 'error'"
|
v-if="message.role === 'error'"
|
||||||
:message="message"
|
:message="message"
|
||||||
:label="labels.error"
|
:label="labels.error"
|
||||||
|
:retry-label="labels.retry"
|
||||||
|
:can-retry="renderMessages[index - 1]?.role === 'user'"
|
||||||
:show-placeholder="sending && index === renderMessages.length - 1"
|
:show-placeholder="sending && index === renderMessages.length - 1"
|
||||||
:variant="variant"
|
:variant="variant"
|
||||||
@copy="emitCopyMessage(message, index)"
|
@copy="emitCopyMessage(message, index)"
|
||||||
|
@retry="emitRetryMessage(message, index)"
|
||||||
@delete="emitDeleteMessage(message, index)"
|
@delete="emitDeleteMessage(message, index)"
|
||||||
/>
|
/>
|
||||||
<ChatAssistantItem
|
<ChatAssistantItem
|
||||||
|
|||||||
@@ -1,14 +1,44 @@
|
|||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
import { cwd } from 'node:process'
|
import { cwd } from 'node:process'
|
||||||
|
|
||||||
|
import Vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
import { playwright } from '@vitest/browser-playwright'
|
||||||
import { loadEnv } from 'vite'
|
import { loadEnv } from 'vite'
|
||||||
import { defineConfig } from 'vitest/config'
|
import { defineConfig } from 'vitest/config'
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
return ({
|
return {
|
||||||
test: {
|
test: {
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
extends: true,
|
||||||
|
test: {
|
||||||
|
name: 'node',
|
||||||
include: ['src/**/*.test.ts'],
|
include: ['src/**/*.test.ts'],
|
||||||
|
exclude: ['src/**/*.browser.test.ts'],
|
||||||
env: loadEnv(mode, join(cwd(), 'packages', 'stage-ui'), ''),
|
env: loadEnv(mode, join(cwd(), 'packages', 'stage-ui'), ''),
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
|
{
|
||||||
|
extends: true,
|
||||||
|
plugins: [
|
||||||
|
Vue(),
|
||||||
|
],
|
||||||
|
test: {
|
||||||
|
name: 'browser',
|
||||||
|
include: ['**/*.browser.{spec,test}.ts'],
|
||||||
|
exclude: ['**/node_modules/**'],
|
||||||
|
browser: {
|
||||||
|
enabled: true,
|
||||||
|
provider: playwright(),
|
||||||
|
instances: [
|
||||||
|
{ browser: 'chromium' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -53,7 +53,12 @@ watch(input, () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
textareaHeight.value = `${textareaRef.value.scrollHeight}px`
|
// NOTICE: not sure why 4px is required but if not added, when
|
||||||
|
// input happened and placeholder now disappeared, the textarea will shrink
|
||||||
|
// a little bit and cause the input box to shake.
|
||||||
|
// TODO: find out the root cause and remove this magic number, or at least
|
||||||
|
// reference a more specific source.
|
||||||
|
textareaHeight.value = `${textareaRef.value.scrollHeight + 4}px`
|
||||||
})
|
})
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ const variantClasses: Record<ButtonVariant, Record<ButtonTheme, {
|
|||||||
'pure': {
|
'pure': {
|
||||||
default: {
|
default: {
|
||||||
default: [
|
default: [
|
||||||
|
'rounded-lg',
|
||||||
'bg-transparent',
|
'bg-transparent',
|
||||||
'text-neutral-900 dark:text-neutral-50',
|
'text-neutral-900 dark:text-neutral-50',
|
||||||
'!px-0 !py-0',
|
'!px-0 !py-0',
|
||||||
@@ -118,10 +119,11 @@ const variantClasses: Record<ButtonVariant, Record<ButtonTheme, {
|
|||||||
'ghost': {
|
'ghost': {
|
||||||
default: {
|
default: {
|
||||||
default: [
|
default: [
|
||||||
|
'rounded-lg',
|
||||||
'bg-transparent',
|
'bg-transparent',
|
||||||
'hover:bg-neutral-100/50 dark:hover:bg-neutral-800/50',
|
'hover:bg-neutral-100/50 dark:hover:bg-neutral-800/50',
|
||||||
'text-neutral-500 dark:text-neutral-400',
|
'text-neutral-500 dark:text-neutral-400',
|
||||||
'focus:ring-2 focus:ring-neutral-300/30 dark:focus:ring-neutral-600/30',
|
'focus:ring-none',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+98
-12
@@ -327,6 +327,9 @@ catalogs:
|
|||||||
vite-plugin-mkcert:
|
vite-plugin-mkcert:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
|
vitest-browser-vue:
|
||||||
|
specifier: ^2.1.0
|
||||||
|
version: 2.1.0
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.32
|
specifier: ^3.5.32
|
||||||
version: 3.5.32
|
version: 3.5.32
|
||||||
@@ -361,9 +364,6 @@ catalogs:
|
|||||||
'@vitest/coverage-v8':
|
'@vitest/coverage-v8':
|
||||||
specifier: ^4.1.4
|
specifier: ^4.1.4
|
||||||
version: 4.1.4
|
version: 4.1.4
|
||||||
vitest:
|
|
||||||
specifier: ^4.1.4
|
|
||||||
version: 4.1.4
|
|
||||||
xsai:
|
xsai:
|
||||||
unspeech:
|
unspeech:
|
||||||
specifier: ^0.1.13
|
specifier: ^0.1.13
|
||||||
@@ -442,6 +442,9 @@ importers:
|
|||||||
'@unocss/preset-web-fonts':
|
'@unocss/preset-web-fonts':
|
||||||
specifier: ^66.6.8
|
specifier: ^66.6.8
|
||||||
version: 66.6.8
|
version: 66.6.8
|
||||||
|
'@vitest/browser-playwright':
|
||||||
|
specifier: catalog:vitest
|
||||||
|
version: 4.1.4(bufferutil@4.1.0)(playwright@1.59.1)(utf-8-validate@5.0.10)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4)
|
||||||
'@vitest/coverage-v8':
|
'@vitest/coverage-v8':
|
||||||
specifier: catalog:vitest
|
specifier: catalog:vitest
|
||||||
version: 4.1.4(@vitest/browser@4.1.4)(vitest@4.1.4)
|
version: 4.1.4(@vitest/browser@4.1.4)(vitest@4.1.4)
|
||||||
@@ -529,6 +532,9 @@ importers:
|
|||||||
vitest:
|
vitest:
|
||||||
specifier: ^4.1.4
|
specifier: ^4.1.4
|
||||||
version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||||
|
vitest-browser-vue:
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 2.1.0(vitest@4.1.4)(vue@3.5.32(typescript@5.9.3))
|
||||||
yaml:
|
yaml:
|
||||||
specifier: ^2.8.3
|
specifier: ^2.8.3
|
||||||
version: 2.8.3
|
version: 2.8.3
|
||||||
@@ -2373,13 +2379,6 @@ importers:
|
|||||||
uncrypto:
|
uncrypto:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 0.1.3
|
version: 0.1.3
|
||||||
devDependencies:
|
|
||||||
'@vitest/browser-playwright':
|
|
||||||
specifier: catalog:vitest
|
|
||||||
version: 4.1.4(bufferutil@4.1.0)(playwright@1.59.1)(utf-8-validate@5.0.10)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4)
|
|
||||||
vitest:
|
|
||||||
specifier: catalog:vitest
|
|
||||||
version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
|
||||||
|
|
||||||
packages/cap-vite:
|
packages/cap-vite:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3448,6 +3447,9 @@ importers:
|
|||||||
vite:
|
vite:
|
||||||
specifier: ^6.4.2
|
specifier: ^6.4.2
|
||||||
version: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
version: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||||
|
vitest-browser-vue:
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 2.1.0(vitest@4.1.4)(vue@3.5.32(typescript@5.9.3))
|
||||||
vue:
|
vue:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 3.5.32(typescript@5.9.3)
|
version: 3.5.32(typescript@5.9.3)
|
||||||
@@ -6926,6 +6928,9 @@ packages:
|
|||||||
'@nxg-org/mineflayer-util-plugin@1.8.4':
|
'@nxg-org/mineflayer-util-plugin@1.8.4':
|
||||||
resolution: {integrity: sha512-hPaCZxU0Aq+gUSi/l6x7n32hUG6bnDugAMoQXD2dFE/gyNkmRSpmgH5+Y6G41w3H8P3Nl++upGCOlaxvZ7RuoA==}
|
resolution: {integrity: sha512-hPaCZxU0Aq+gUSi/l6x7n32hUG6bnDugAMoQXD2dFE/gyNkmRSpmgH5+Y6G41w3H8P3Nl++upGCOlaxvZ7RuoA==}
|
||||||
|
|
||||||
|
'@one-ini/wasm@0.1.1':
|
||||||
|
resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==}
|
||||||
|
|
||||||
'@opentelemetry/api-logs@0.215.0':
|
'@opentelemetry/api-logs@0.215.0':
|
||||||
resolution: {integrity: sha512-xrFlqhdhUyO8wSRn6DjE0145/HPWSJ5Nm0C7vWua6TdL/FSEAZvEyvdsa9CRXuxo9ebb7j/NEPhEcO62IJ0qUA==}
|
resolution: {integrity: sha512-xrFlqhdhUyO8wSRn6DjE0145/HPWSJ5Nm0C7vWua6TdL/FSEAZvEyvdsa9CRXuxo9ebb7j/NEPhEcO62IJ0qUA==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
@@ -10392,6 +10397,9 @@ packages:
|
|||||||
'@vue/shared@3.5.32':
|
'@vue/shared@3.5.32':
|
||||||
resolution: {integrity: sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==}
|
resolution: {integrity: sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==}
|
||||||
|
|
||||||
|
'@vue/test-utils@2.4.6':
|
||||||
|
resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==}
|
||||||
|
|
||||||
'@vue/tsconfig@0.9.1':
|
'@vue/tsconfig@0.9.1':
|
||||||
resolution: {integrity: sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==}
|
resolution: {integrity: sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -10606,6 +10614,10 @@ packages:
|
|||||||
'@xsai/utils-chat@0.5.0-beta.2':
|
'@xsai/utils-chat@0.5.0-beta.2':
|
||||||
resolution: {integrity: sha512-WSjoxY0w+3oRSZQkAWHdhOuidRlaYJR3y6bvELmJje6lCh86a4PQtMIPwJIoTmtuVY13ZgGvvKqoQz/0xS32LA==}
|
resolution: {integrity: sha512-WSjoxY0w+3oRSZQkAWHdhOuidRlaYJR3y6bvELmJje6lCh86a4PQtMIPwJIoTmtuVY13ZgGvvKqoQz/0xS32LA==}
|
||||||
|
|
||||||
|
abbrev@2.0.0:
|
||||||
|
resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
|
||||||
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
|
|
||||||
abbrev@3.0.1:
|
abbrev@3.0.1:
|
||||||
resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==}
|
resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==}
|
||||||
engines: {node: ^18.17.0 || >=20.5.0}
|
engines: {node: ^18.17.0 || >=20.5.0}
|
||||||
@@ -11442,6 +11454,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==}
|
resolution: {integrity: sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==}
|
||||||
engines: {node: '>=12.20.0'}
|
engines: {node: '>=12.20.0'}
|
||||||
|
|
||||||
|
commander@10.0.1:
|
||||||
|
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
commander@11.1.0:
|
commander@11.1.0:
|
||||||
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
|
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
@@ -12238,6 +12254,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-dS5cbA9rA2VR4Ybuvhg6jvdmp46ubLn3E+px8cG/35aEDNclrqoCjg6mt0HYZ/M+OoESS3jSkCrqk1kWAEhWAw==}
|
resolution: {integrity: sha512-dS5cbA9rA2VR4Ybuvhg6jvdmp46ubLn3E+px8cG/35aEDNclrqoCjg6mt0HYZ/M+OoESS3jSkCrqk1kWAEhWAw==}
|
||||||
engines: {bun: '>=1', deno: '>=2', node: '>=16'}
|
engines: {bun: '>=1', deno: '>=2', node: '>=16'}
|
||||||
|
|
||||||
|
editorconfig@1.0.7:
|
||||||
|
resolution: {integrity: sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
ee-first@1.1.1:
|
ee-first@1.1.1:
|
||||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
|
|
||||||
@@ -13900,6 +13921,15 @@ packages:
|
|||||||
jpeg-js@0.4.4:
|
jpeg-js@0.4.4:
|
||||||
resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==}
|
resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==}
|
||||||
|
|
||||||
|
js-beautify@1.15.4:
|
||||||
|
resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
js-cookie@3.0.5:
|
||||||
|
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
js-tokens@10.0.0:
|
js-tokens@10.0.0:
|
||||||
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
|
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
|
||||||
|
|
||||||
@@ -14952,6 +14982,11 @@ packages:
|
|||||||
node-vibrant@4.0.4:
|
node-vibrant@4.0.4:
|
||||||
resolution: {integrity: sha512-hA/pUXBE9TJ41G9FlTkzeqD5JdxgvvPGYZb/HNpdkaxxXUEnP36imSolZ644JuPun+lTd+FpWWtBpTYdp2noQA==}
|
resolution: {integrity: sha512-hA/pUXBE9TJ41G9FlTkzeqD5JdxgvvPGYZb/HNpdkaxxXUEnP36imSolZ644JuPun+lTd+FpWWtBpTYdp2noQA==}
|
||||||
|
|
||||||
|
nopt@7.2.1:
|
||||||
|
resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==}
|
||||||
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
nopt@8.1.0:
|
nopt@8.1.0:
|
||||||
resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==}
|
resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==}
|
||||||
engines: {node: ^18.17.0 || >=20.5.0}
|
engines: {node: ^18.17.0 || >=20.5.0}
|
||||||
@@ -17631,6 +17666,12 @@ packages:
|
|||||||
postcss:
|
postcss:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
vitest-browser-vue@2.1.0:
|
||||||
|
resolution: {integrity: sha512-K3H/oxIOY4EjXx2bxwrLsPg4jTMvzpRW6Jb6T8XZRoxUvmDVwGkd8mua130F8GZezE7H5QYoXd/S8hYijw7j5g==}
|
||||||
|
peerDependencies:
|
||||||
|
vitest: ^4.0.0-0
|
||||||
|
vue: ^3.0.0
|
||||||
|
|
||||||
vitest@4.1.4:
|
vitest@4.1.4:
|
||||||
resolution: {integrity: sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==}
|
resolution: {integrity: sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==}
|
||||||
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
|
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||||
@@ -17679,6 +17720,9 @@ packages:
|
|||||||
vscode-uri@3.1.0:
|
vscode-uri@3.1.0:
|
||||||
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
|
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
|
||||||
|
|
||||||
|
vue-component-type-helpers@2.2.12:
|
||||||
|
resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==}
|
||||||
|
|
||||||
vue-demi@0.14.10:
|
vue-demi@0.14.10:
|
||||||
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -21045,6 +21089,8 @@ snapshots:
|
|||||||
|
|
||||||
'@nxg-org/mineflayer-util-plugin@1.8.4': {}
|
'@nxg-org/mineflayer-util-plugin@1.8.4': {}
|
||||||
|
|
||||||
|
'@one-ini/wasm@0.1.1': {}
|
||||||
|
|
||||||
'@opentelemetry/api-logs@0.215.0':
|
'@opentelemetry/api-logs@0.215.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.1
|
'@opentelemetry/api': 1.9.1
|
||||||
@@ -24147,7 +24193,6 @@ snapshots:
|
|||||||
- msw
|
- msw
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
- vite
|
- vite
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@vitest/browser-playwright@4.1.4(bufferutil@4.1.0)(playwright@1.59.1)(utf-8-validate@5.0.10)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4)':
|
'@vitest/browser-playwright@4.1.4(bufferutil@4.1.0)(playwright@1.59.1)(utf-8-validate@5.0.10)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -24161,6 +24206,7 @@ snapshots:
|
|||||||
- msw
|
- msw
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
- vite
|
- vite
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@vitest/browser@4.1.4(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4)':
|
'@vitest/browser@4.1.4(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -24178,7 +24224,6 @@ snapshots:
|
|||||||
- msw
|
- msw
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
- vite
|
- vite
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@vitest/browser@4.1.4(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4)':
|
'@vitest/browser@4.1.4(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -24196,6 +24241,7 @@ snapshots:
|
|||||||
- msw
|
- msw
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
- vite
|
- vite
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@vitest/coverage-v8@4.1.4(@vitest/browser@4.1.4)(vitest@4.1.4)':
|
'@vitest/coverage-v8@4.1.4(@vitest/browser@4.1.4)(vitest@4.1.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -24249,6 +24295,7 @@ snapshots:
|
|||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@vitest/pretty-format@4.1.4':
|
'@vitest/pretty-format@4.1.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -24769,6 +24816,11 @@ snapshots:
|
|||||||
|
|
||||||
'@vue/shared@3.5.32': {}
|
'@vue/shared@3.5.32': {}
|
||||||
|
|
||||||
|
'@vue/test-utils@2.4.6':
|
||||||
|
dependencies:
|
||||||
|
js-beautify: 1.15.4
|
||||||
|
vue-component-type-helpers: 2.2.12
|
||||||
|
|
||||||
'@vue/tsconfig@0.9.1(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3))':
|
'@vue/tsconfig@0.9.1(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3))':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
@@ -25021,6 +25073,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@xsai/shared-chat': 0.5.0-beta.2(patch_hash=26f2819b987245ec85f216b821ddf73aeb28fd7e611238a2d37250e42f838a01)
|
'@xsai/shared-chat': 0.5.0-beta.2(patch_hash=26f2819b987245ec85f216b821ddf73aeb28fd7e611238a2d37250e42f838a01)
|
||||||
|
|
||||||
|
abbrev@2.0.0: {}
|
||||||
|
|
||||||
abbrev@3.0.1: {}
|
abbrev@3.0.1: {}
|
||||||
|
|
||||||
abort-controller@3.0.0:
|
abort-controller@3.0.0:
|
||||||
@@ -25864,6 +25918,8 @@ snapshots:
|
|||||||
table-layout: 4.1.1
|
table-layout: 4.1.1
|
||||||
typical: 7.3.0
|
typical: 7.3.0
|
||||||
|
|
||||||
|
commander@10.0.1: {}
|
||||||
|
|
||||||
commander@11.1.0: {}
|
commander@11.1.0: {}
|
||||||
|
|
||||||
commander@12.1.0: {}
|
commander@12.1.0: {}
|
||||||
@@ -26497,6 +26553,13 @@ snapshots:
|
|||||||
'@noble/curves': 1.9.7
|
'@noble/curves': 1.9.7
|
||||||
'@noble/hashes': 1.8.0
|
'@noble/hashes': 1.8.0
|
||||||
|
|
||||||
|
editorconfig@1.0.7:
|
||||||
|
dependencies:
|
||||||
|
'@one-ini/wasm': 0.1.1
|
||||||
|
commander: 10.0.1
|
||||||
|
minimatch: 9.0.5
|
||||||
|
semver: 7.7.4
|
||||||
|
|
||||||
ee-first@1.1.1: {}
|
ee-first@1.1.1: {}
|
||||||
|
|
||||||
ejs@3.1.10:
|
ejs@3.1.10:
|
||||||
@@ -28574,6 +28637,16 @@ snapshots:
|
|||||||
|
|
||||||
jpeg-js@0.4.4: {}
|
jpeg-js@0.4.4: {}
|
||||||
|
|
||||||
|
js-beautify@1.15.4:
|
||||||
|
dependencies:
|
||||||
|
config-chain: 1.1.13
|
||||||
|
editorconfig: 1.0.7
|
||||||
|
glob: 10.5.0
|
||||||
|
js-cookie: 3.0.5
|
||||||
|
nopt: 7.2.1
|
||||||
|
|
||||||
|
js-cookie@3.0.5: {}
|
||||||
|
|
||||||
js-tokens@10.0.0: {}
|
js-tokens@10.0.0: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
@@ -29939,6 +30012,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
|
|
||||||
|
nopt@7.2.1:
|
||||||
|
dependencies:
|
||||||
|
abbrev: 2.0.0
|
||||||
|
|
||||||
nopt@8.1.0:
|
nopt@8.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
abbrev: 3.0.1
|
abbrev: 3.0.1
|
||||||
@@ -33330,6 +33407,12 @@ snapshots:
|
|||||||
- sortablejs
|
- sortablejs
|
||||||
- universal-cookie
|
- universal-cookie
|
||||||
|
|
||||||
|
vitest-browser-vue@2.1.0(vitest@4.1.4)(vue@3.5.32(typescript@5.9.3)):
|
||||||
|
dependencies:
|
||||||
|
'@vue/test-utils': 2.4.6
|
||||||
|
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||||
|
vue: 3.5.32(typescript@5.9.3)
|
||||||
|
|
||||||
vitest@4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
vitest@4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 4.1.4
|
'@vitest/expect': 4.1.4
|
||||||
@@ -33391,6 +33474,7 @@ snapshots:
|
|||||||
jsdom: 27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10)
|
jsdom: 27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- msw
|
- msw
|
||||||
|
optional: true
|
||||||
|
|
||||||
vscode-ext-gen@1.6.0:
|
vscode-ext-gen@1.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -33399,6 +33483,8 @@ snapshots:
|
|||||||
|
|
||||||
vscode-uri@3.1.0: {}
|
vscode-uri@3.1.0: {}
|
||||||
|
|
||||||
|
vue-component-type-helpers@2.2.12: {}
|
||||||
|
|
||||||
vue-demi@0.14.10(vue@3.5.32(typescript@5.9.3)):
|
vue-demi@0.14.10(vue@3.5.32(typescript@5.9.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.5.32(typescript@5.9.3)
|
vue: 3.5.32(typescript@5.9.3)
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ catalog:
|
|||||||
vite: ^8.0.8
|
vite: ^8.0.8
|
||||||
vite-plugin-inspect: 12.0.0-beta.1
|
vite-plugin-inspect: 12.0.0-beta.1
|
||||||
vite-plugin-mkcert: ^2.0.0
|
vite-plugin-mkcert: ^2.0.0
|
||||||
|
vitest-browser-vue: ^2.1.0
|
||||||
vue: ^3.5.32
|
vue: ^3.5.32
|
||||||
vue-router: ^5.0.4
|
vue-router: ^5.0.4
|
||||||
vue-sonner: 2.0.9
|
vue-sonner: 2.0.9
|
||||||
|
|||||||
Reference in New Issue
Block a user