Files
new-api/web/default/src/features/playground/lib/message/message-utils.ts
T
QuentinHsu 966af88ec3 feat(playground): improve Playground chat experience and Markdown rendering (#5217)
* refactor(playground): streamline chat request state

- extract conversation actions from the page component to keep message flow logic reusable.
- unify streaming and non-streaming generation state, including abort support for non-stream requests.
- simplify message rendering and payload construction while localizing Playground prompts.

* fix(playground): validate persisted chat state

- wrap saved Playground state with a storage version while still reading legacy values.

- validate config, parameter toggles, and messages before restoring them from localStorage.

- cap stored chat history to the latest messages to avoid oversized or stale state.

* refactor(playground): centralize message content access

- route chat rendering, copy actions, and error display through shared message helpers.

- reuse the current-version update helper for non-streaming assistant responses.

- keep message version details behind utility functions to reduce future model churn.

* refactor(playground): split storage schemas

- move Playground storage validation schemas into a dedicated module.

- keep storage read and write logic focused on migration, trimming, and persistence.

- preserve the existing storage envelope and validation behavior.

* refactor(playground): extract options loading hook

- move model and group queries into a dedicated hook so the page component stays focused on layout wiring.
- preserve existing fallback selection and error toast behavior while reusing the hook through the playground barrel export.

* refactor(playground): extract prompt suggestions

- move static prompt suggestion rendering into a focused component so the input stays centered on compose controls.
- preserve translated suggestion submission behavior while isolating icon metadata from the input form.

* refactor(playground): extract input tools

- move attachment and search controls into a dedicated component so the prompt input stays focused on compose state.
- keep existing development toast behavior and disabled handling while centralizing tool metadata.

* refactor(playground): extract input controls

- move model, group, send, and stop controls into a focused component so the input only manages compose state.
- preserve existing disabled states and generation button behavior while isolating control rendering.

* refactor(playground): extract message content display

- move sources, reasoning, loading, error, and response rendering into a dedicated message content component.
- keep the chat list focused on message iteration, edit state, and action wiring without changing display behavior.

* refactor(playground): extract message editor

- move inline message editing controls into a dedicated editor component so the chat list stays focused on rendering flow.
- preserve save, save-and-submit, cancel, and disabled-state behavior for edited messages.

* refactor(playground): extract stream error parsing

- move SSE error payload parsing into a reusable stream utility so the request hook stays focused on lifecycle handling.
- preserve existing error message, error code, and fallback behavior for raw or empty stream errors.

* refactor(playground): extract request error parsing

- move non-stream request error extraction into a shared utility so the chat handler stays focused on request flow.
- preserve the existing response message, error code, and fallback priority for failed chat completions.

* refactor(playground): extract streaming chunk updates

- move reasoning and content chunk application into a message utility so the chat handler only wires stream events.
- preserve error-state skipping, reasoning accumulation, and content streaming behavior for assistant messages.

* refactor(playground): extract message reasoning parser

- move think tag parsing into a dedicated playground message utility.
- export the parser through the shared playground lib barrel for consistent imports.

* refactor(playground): extract message streaming utilities

- move stream chunk application and message finalization into a dedicated utility.
- keep stored message sanitization with the streaming lifecycle helpers.

* refactor(playground): extract message update utilities

- move assistant message update helpers into a focused playground utility.
- keep error-state message updates separate from core message construction helpers.

* refactor(playground): extract completion choice handling

- move non-streaming choice application into the message streaming utilities.
- keep the chat handler focused on request orchestration and message updates.

* refactor(playground): centralize assistant completion state

- add a helper for finalizing assistant messages with complete status.
- reuse the helper in stream completion and stop-generation paths.

* refactor(playground): extract stream message parsing

- move SSE delta parsing into a shared stream utility.
- keep the stream request hook focused on lifecycle handling and update dispatch.

* refactor(playground): extract stream ready state checks

- move SSE ready-state status handling into stream utilities.
- keep weak source status typing outside the stream request hook.

* refactor(playground): extract conversation message helpers

- move send, regenerate, and edit message list construction into focused utilities.
- keep the conversation hook focused on edit state and update dispatch.

* refactor(playground): extract state initialization helpers

- move playground initial state loading into focused utility helpers.
- centralize message state updater resolution outside the React state hook.

* refactor(playground): extract option fallback helpers

- move model and group fallback selection into focused playground utilities.
- keep the options hook focused on query results, toasts, and config updates.

* refactor(playground): extract message action helpers

- move message action state derivation into focused utilities.

- keep the action component focused on guarded handlers and rendering.

* refactor(playground): extract input control state

- move submit, stop, and selector state derivation into a pure helper.

- keep input controls focused on rendering model selectors and action buttons.

* refactor(playground): extract message content state

- move source, reasoning, loader, and body visibility checks into a pure helper.

- use a discriminated state shape so rendered reasoning content stays type-safe.

* refactor(playground): extract message editor state

- move save eligibility and submit visibility checks into a pure helper.

- keep the editor component focused on textarea and button rendering.

* refactor(playground): extract message error state

- move error kind, fallback content, and admin visibility checks into a pure helper.

- centralize the model pricing settings path used by the error action.

* refactor(playground): extract chat render state

- move editing content lookup and per-message render flags into conversation helpers.

- keep the chat component focused on mapping messages to editor and content views.

* refactor(playground): extract suggestion display state

- move suggestion class selection into a pure helper.

- keep the suggestions component focused on translation and rendering.

* refactor(playground): extract assistant message state checks

- move final and pending assistant status checks into streaming utilities.

- keep the chat handler focused on request lifecycle updates.

* refactor(playground): extract input tool state

- move attachment action metadata and development notices into input tool utilities.

- keep the input tools component focused on menu and button rendering.

* refactor(playground): extract stream protocol checks

- move SSE done-message and closed-ready-state checks into stream utilities.

- keep the stream request hook focused on event handling flow.

* refactor(playground): extract message removal helper

- move delete-message filtering into conversation message utilities.

- keep the conversation hook focused on action orchestration.

* refactor(playground): extract option error messages

- move option load error message selection into playground option utilities
- keep the options hook focused on query effects and fallback updates

* refactor(playground): extract input submit text helper

- move prompt submit text validation into input control utilities
- let the input component submit only when a concrete text value is available

* refactor(playground): centralize error message checks

- add a shared helper for identifying error messages
- remove direct status string checks from message content rendering

* refactor(playground): extract message content display checks

- move loader and content visibility decisions into local helper functions
- keep message content state assembly focused on composing render state

* refactor(playground): replace raw message role checks

- use shared message role constants in conversation edit handling
- avoid raw assistant role literals when validating API messages

* refactor(playground): extract non-stream response handling

- move chat completion response choice handling into message streaming utilities
- keep the chat handler focused on request lifecycle and error routing

* refactor(playground): centralize stream cleanup

- reuse one stream cleanup path for completion, errors, startup failures, and manual stops
- preserve the current-source guard when closing SSE streams

* refactor(playground): extract pending assistant check

- centralize pending assistant message detection in streaming utilities
- reuse the helper when sanitizing stored playground messages

* perf(playground): improve mobile input controls

- split mobile input controls into selector and action rows
- keep the desktop input footer compact while reducing mobile control crowding

* perf(playground): add starter empty state

- show starter prompts in the empty playground chat area
- wire empty-state prompt selection into the existing send flow
- add localized copy for the new empty state

* perf(playground): improve mobile message actions

- collapse mobile message actions into a touch-friendly dropdown menu
- keep the desktop hover action strip unchanged for pointer workflows
- share one action list between desktop buttons and the mobile menu

* perf(playground): add error recovery actions

- show retry, edit, and delete actions inside error message alerts
- route edit recovery to the previous user prompt when available
- keep recovery controls touch-friendly on mobile layouts

* perf(playground): refine message editing experience

- present message edits in a focused bordered editor panel
- add unsaved-change state, reset, and cancel confirmation flows
- improve mobile touch targets and keyboard shortcuts for editing

* perf(playground): improve markdown code blocks

- render fenced markdown code with syntax highlighting, line numbers, and fallback plain text
- add copy, download, and collapse controls for playground AI responses
- tighten code block layout and theme token styles for responsive markdown rendering

* fix(playground): constrain markdown code block height

- collapse long playground code blocks after a short preview instead of waiting for very large snippets
- cap expanded code blocks so long responses scroll inside the code block
- keep generic code block usage unconstrained unless a caller opts in

* feat(playground): add chat history clearing

- add a toolbar action that is enabled only when saved playground messages exist.
- confirm destructive clears before removing browser-stored conversation state.
- add localized strings for the action, dialog, and completion toast.

* perf(playground): improve chat markdown rendering

- refine assistant and user message surfaces so chat content matches the app UI.
- normalize markdown typography, tables, images, lists, blockquotes, and details rendering.
- add indentation cues for collapsible reasoning and source sections.

* style: format code block component

* style: format playground frontend files

* feat(playground): render markdown with stream parser

- replace Streamdown with stream-markdown-parser for project-owned markdown rendering and styling.
- split response rendering into focused block, inline, table, alert, details, and footnote modules.
- pass message final state into response parsing so streaming content can be parsed incrementally.

* fix(playground): localize reasoning and chat feedback

- translate reasoning status, message actions, playground errors, and response renderer fallbacks across supported locales.
- keep reasoning duration numeric and tighten the collapsible layout to prevent trigger jitter.
- register dynamic keys so i18n sync keeps runtime labels covered.

* refactor(playground): group files by functional area

- move chat, input, and message components into focused subdirectories to make the UI structure easier to scan.
- split playground helpers into input, message, streaming, storage, options, state, and suggestions modules.
- update barrel exports and imports so existing feature entry points continue to work.

* fix(playground): prevent history replay from freezing page

- defer saved conversation loading so route entry no longer blocks on localStorage parsing and markdown rendering.
- limit initial history rendering and skip expensive markdown parsing for oversized responses.
- normalize corrupted streaming snapshots and cumulative chunks to keep saved playground history bounded.
- add message timing metadata and layout alignment groundwork without introducing live timers.

* feat(playground): allow regenerating from user messages

- show regenerate actions on user messages with saved content.
- truncate following conversation state before starting a fresh assistant response.

* feat(playground): add raw response source view

- add a per-message source toggle for assistant responses.
- render raw response content with the existing code block viewer.
- localize the new source and preview action labels.

* feat(playground): render code with unified editor

- replace Shiki HTML rendering with a read-only CodeMirror view for code blocks and raw responses.
- reuse the same CodeMirror frame for message editing so source and edit modes stay visually aligned.
- add lightweight CodeMirror dependencies while keeping language support scoped to Markdown.

* perf(playground): streamline chat input controls

- combine model and group selection into one compact picker for faster context switching.
- switch playground action buttons to icon-first controls with tooltips to reduce toolbar width.
- refresh input footer styling and submit states so active and destructive actions are clearer.
- bump dompurify lockfile entry to keep the frontend dependency current.

* fix(playground): filter models by selected group

- query user models by the selected playground group instead of reusing the cross-group model union.
- clear unavailable model selections and block sending when the active group has no models.
- align model selector and error action controls with the existing playground interaction style.

* perf(playground): remove input suggestion chips

- remove the prompt suggestion row below the playground input to reduce visual noise.
- delete the now-unused suggestion component and display helper.

* perf(playground): stabilize reasoning trigger layout

- use fixed icon slots around the reasoning label so the left content stays still when toggling.
- limit the open state animation to the chevron rotation for a smoother collapse interaction.

* perf(playground): smooth reasoning expansion

- use the collapsible panel height animation for vertical reasoning reveals.
- sync inner content opacity and position with the panel state.
2026-06-27 17:23:25 +08:00

177 lines
4.1 KiB
TypeScript
Vendored

/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { nanoid } from 'nanoid'
import { MESSAGE_ROLES, MESSAGE_STATUS } from '../../constants'
import type {
Message,
MessageVersion,
ChatCompletionMessage,
ContentPart,
} from '../../types'
/**
* Create a new message version
*/
export function createMessageVersion(content: string): MessageVersion {
return {
id: nanoid(),
content,
}
}
/**
* Get current version from message (always returns the first version)
*/
export function getCurrentVersion(message: Message): MessageVersion {
return message.versions[0] || { id: 'default', content: '' }
}
/**
* Get displayable content from the current message version.
*/
export function getMessageContent(message: Message): string {
return getCurrentVersion(message).content
}
/**
* Check whether a message has non-empty content in its current version.
*/
export function hasMessageContent(message: Message): boolean {
return getMessageContent(message).trim() !== ''
}
/**
* Update current version content in message
*/
export function updateCurrentVersionContent(
message: Message,
content: string
): Message {
const currentVersion = getCurrentVersion(message)
return {
...message,
versions: [{ ...currentVersion, content }],
}
}
/**
* Create a user message
*/
export function createUserMessage(
content: string,
createdAt: number = Date.now()
): Message {
return {
key: nanoid(),
from: MESSAGE_ROLES.USER,
versions: [createMessageVersion(content)],
createdAt,
}
}
/**
* Create a loading assistant message
*/
export function createLoadingAssistantMessage(
startedAt: number = Date.now()
): Message {
return {
key: nanoid(),
from: MESSAGE_ROLES.ASSISTANT,
versions: [createMessageVersion('')],
createdAt: startedAt,
startedAt,
reasoning: undefined,
isReasoningComplete: false,
isContentComplete: false,
isReasoningStreaming: false,
status: MESSAGE_STATUS.LOADING,
}
}
/**
* Build message content with optional images
*/
export function buildMessageContent(
text: string,
imageUrls: string[] = []
): string | ContentPart[] {
const validImages = imageUrls.filter((url) => url.trim() !== '')
if (validImages.length === 0) {
return text
}
const parts: ContentPart[] = [
{
type: 'text',
text: text || '',
},
...validImages.map((url) => ({
type: 'image_url' as const,
image_url: { url: url.trim() },
})),
]
return parts
}
/**
* Extract text content from message content
*/
export function getTextContent(content: string | ContentPart[]): string {
if (typeof content === 'string') {
return content
}
if (Array.isArray(content)) {
const textPart = content.find((part) => part.type === 'text')
return textPart?.text || ''
}
return ''
}
/**
* Format message for API request
*/
export function formatMessageForAPI(message: Message): ChatCompletionMessage {
const currentVersion = getCurrentVersion(message)
return {
role: message.from,
content: currentVersion.content,
}
}
/**
* Check if message is valid for API request
* Excludes loading/streaming assistant messages and empty content
*/
export function isValidMessage(message: Message): boolean {
if (!message || !message.from || !message.versions.length) return false
// Exclude empty assistant messages (loading/streaming placeholders)
if (message.from === MESSAGE_ROLES.ASSISTANT && !hasMessageContent(message)) {
return false
}
return true
}