mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 16:03:45 +00:00
115 lines
4.3 KiB
TypeScript
115 lines
4.3 KiB
TypeScript
import { formatAgentTypeLabel } from '../../../src/shared/agent-type-label'
|
|
import {
|
|
formatNativeChatEmptyStateCopy,
|
|
type NativeChatEmptyStateCopy
|
|
} from '../../../src/shared/native-chat-empty-state'
|
|
import { stripNoiseMessages } from '../../../src/shared/native-chat-noise'
|
|
import { foldToolMessages } from '../../../src/shared/native-chat-tool-fold'
|
|
import { isImageRefBlock, type NativeChatMessage } from '../../../src/shared/native-chat-types'
|
|
import { normalizeImageTranscriptMessages } from './mobile-native-chat-image-transcript-markers'
|
|
import type { MobileNativeChatStatus } from './use-mobile-native-chat-session'
|
|
|
|
/** The centered empty-state copy for a chat with no messages, mirroring the
|
|
* desktop `NativeChatEmptyState` (shared copy + agent label) so the two surfaces
|
|
* stay in lockstep. Returns null when the list should stay bare (idle, or the
|
|
* loading spinner owns the view). */
|
|
export function mobileNativeChatEmptyState(
|
|
status: MobileNativeChatStatus,
|
|
agent: string | null,
|
|
error?: string
|
|
): NativeChatEmptyStateCopy | null {
|
|
const agentLabel = agent ? formatAgentTypeLabel(agent) : 'the agent'
|
|
switch (status) {
|
|
// A live agent with no transcript yet — and a loaded-but-empty transcript —
|
|
// are both "start a chat"; invite the first message instead of implying the
|
|
// agent is still starting up.
|
|
case 'waiting-session':
|
|
case 'ready':
|
|
return formatNativeChatEmptyStateCopy('empty', agentLabel)
|
|
case 'error': {
|
|
const copy = formatNativeChatEmptyStateCopy('error', agentLabel)
|
|
return error ? { ...copy, subtitle: error } : copy
|
|
}
|
|
default:
|
|
return null
|
|
}
|
|
}
|
|
|
|
/** An optimistic user echo: the text and/or the local preview URIs of any images
|
|
* ridden along on the send, shown until the transcript catches up. */
|
|
export type MobileNativeChatPendingItem = {
|
|
id: string
|
|
text: string
|
|
images?: string[]
|
|
}
|
|
|
|
export function foldMobileNativeChatMessages(messages: NativeChatMessage[]): NativeChatMessage[] {
|
|
// Normalize first (desktop assembler parity): image marker turns fold into
|
|
// image-ref blocks instead of rendering as raw `[Image: …]` text.
|
|
return foldToolMessages(stripNoiseMessages(normalizeImageTranscriptMessages(messages)))
|
|
}
|
|
|
|
/** Assemble the list data the chat renders: the folded transcript, then a
|
|
* synthetic bubble for the streaming text the gate let through, then the
|
|
* route-owned accepted optimistic messages at the tail. */
|
|
export function buildMobileNativeChatTransientData({
|
|
folded,
|
|
streaming,
|
|
pending,
|
|
imagePreviewsByMessageId
|
|
}: {
|
|
folded: NativeChatMessage[]
|
|
/** Streaming bubble text, already gated by `deriveMobileNativeChatStreaming`. */
|
|
streaming: string | null
|
|
pending: MobileNativeChatPendingItem[]
|
|
imagePreviewsByMessageId?: Record<string, string[]>
|
|
}): { folded: NativeChatMessage[]; streaming: string | null; data: NativeChatMessage[] } {
|
|
const renderedFolded = folded.map((message) => {
|
|
const previews = imagePreviewsByMessageId?.[message.id]
|
|
if (message.role !== 'user' || !previews?.length) {
|
|
return message
|
|
}
|
|
let previewIndex = 0
|
|
const blocks = message.blocks.map((block) => {
|
|
if (!isImageRefBlock(block)) {
|
|
return block
|
|
}
|
|
const url = previews[previewIndex]
|
|
previewIndex += 1
|
|
return url ? { ...block, url } : block
|
|
})
|
|
while (previewIndex < previews.length) {
|
|
blocks.push({ type: 'image-ref', url: previews[previewIndex] })
|
|
previewIndex += 1
|
|
}
|
|
return { ...message, blocks }
|
|
})
|
|
const data: NativeChatMessage[] = [
|
|
...renderedFolded,
|
|
...(streaming
|
|
? [
|
|
{
|
|
id: 'streaming',
|
|
role: 'assistant' as const,
|
|
blocks: [{ type: 'text' as const, text: streaming }],
|
|
timestamp: null,
|
|
source: 'hook' as const
|
|
}
|
|
]
|
|
: []),
|
|
...pending.map((p) => ({
|
|
id: p.id,
|
|
role: 'user' as const,
|
|
// Text first (when present), then a thumbnail per ridden-along image so the
|
|
// sent photo shows immediately, before the transcript echo lands.
|
|
blocks: [
|
|
...(p.text ? [{ type: 'text' as const, text: p.text }] : []),
|
|
...(p.images ?? []).map((uri) => ({ type: 'image-ref' as const, url: uri }))
|
|
],
|
|
timestamp: null,
|
|
source: 'transcript' as const
|
|
}))
|
|
]
|
|
return { folded: renderedFolded, streaming, data }
|
|
}
|