Webchat: distinguish system messages
This commit is contained in:
parent
7f6422c897
commit
de780ee6d8
@ -18,6 +18,21 @@
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.chat-group.system {
|
||||
justify-content: center;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.chat-group.system .chat-group-messages {
|
||||
align-items: center;
|
||||
max-width: min(820px, 90%);
|
||||
}
|
||||
|
||||
.chat-group.system .chat-group-footer {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chat-group-messages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -89,6 +104,10 @@
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.chat-group.system .chat-avatar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Image avatar support */
|
||||
img.chat-avatar {
|
||||
display: block;
|
||||
@ -228,6 +247,21 @@ img.chat-avatar {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.chat-group.system .chat-bubble {
|
||||
background: var(--bg-muted);
|
||||
border-color: var(--border);
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
padding: 8px 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .chat-group.system .chat-bubble {
|
||||
background: rgba(148, 163, 184, 0.12);
|
||||
border-color: rgba(148, 163, 184, 0.35);
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .chat-group.user .chat-bubble {
|
||||
border-color: rgba(234, 88, 12, 0.2);
|
||||
background: rgba(251, 146, 60, 0.12);
|
||||
|
||||
@ -78,12 +78,16 @@ export function renderMessageGroup(
|
||||
? "You"
|
||||
: normalizedRole === "assistant"
|
||||
? assistantName
|
||||
: normalizedRole === "system"
|
||||
? "System"
|
||||
: normalizedRole;
|
||||
const roleClass =
|
||||
normalizedRole === "user"
|
||||
? "user"
|
||||
: normalizedRole === "assistant"
|
||||
? "assistant"
|
||||
: normalizedRole === "system"
|
||||
? "system"
|
||||
: "other";
|
||||
const timestamp = new Date(group.timestamp).toLocaleTimeString([], {
|
||||
hour: "numeric",
|
||||
@ -129,6 +133,8 @@ function renderAvatar(
|
||||
? "U"
|
||||
: normalized === "assistant"
|
||||
? assistantName.charAt(0).toUpperCase() || "A"
|
||||
: normalized === "system"
|
||||
? "!"
|
||||
: normalized === "tool"
|
||||
? "⚙"
|
||||
: "?";
|
||||
@ -137,6 +143,8 @@ function renderAvatar(
|
||||
? "user"
|
||||
: normalized === "assistant"
|
||||
? "assistant"
|
||||
: normalized === "system"
|
||||
? "system"
|
||||
: normalized === "tool"
|
||||
? "tool"
|
||||
: "other";
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
normalizeMessage,
|
||||
normalizeRoleForGrouping,
|
||||
isToolResultMessage,
|
||||
splitSystemPreface,
|
||||
} from "./message-normalizer";
|
||||
|
||||
describe("message-normalizer", () => {
|
||||
@ -138,6 +139,48 @@ describe("message-normalizer", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("splitSystemPreface", () => {
|
||||
it("splits leading system block into system message", () => {
|
||||
const normalized = normalizeMessage({
|
||||
role: "user",
|
||||
content: "System: [2026-01-26 10:00:00 UTC] Exec finished\n\nRun complete",
|
||||
timestamp: 123,
|
||||
});
|
||||
const result = splitSystemPreface(normalized);
|
||||
expect(result.systemMessage?.role).toBe("system");
|
||||
expect(result.systemMessage?.content).toEqual([
|
||||
{ type: "text", text: expect.stringContaining("Exec finished") },
|
||||
]);
|
||||
expect(result.message.content).toEqual([
|
||||
{ type: "text", text: "Run complete" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps user message when no system prefix", () => {
|
||||
const normalized = normalizeMessage({
|
||||
role: "user",
|
||||
content: "Hello",
|
||||
timestamp: 123,
|
||||
});
|
||||
const result = splitSystemPreface(normalized);
|
||||
expect(result.systemMessage).toBeNull();
|
||||
expect(result.message.content).toEqual([{ type: "text", text: "Hello" }]);
|
||||
});
|
||||
|
||||
it("creates system message even when remainder is empty", () => {
|
||||
const normalized = normalizeMessage({
|
||||
role: "user",
|
||||
content: "System: [2026-01-26 10:00:00 UTC] Gateway restart",
|
||||
timestamp: 123,
|
||||
});
|
||||
const result = splitSystemPreface(normalized);
|
||||
expect(result.systemMessage?.content).toEqual([
|
||||
{ type: "text", text: expect.stringContaining("Gateway restart") },
|
||||
]);
|
||||
expect(result.message.content).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isToolResultMessage", () => {
|
||||
it("returns true for toolresult role", () => {
|
||||
expect(isToolResultMessage({ role: "toolresult" })).toBe(true);
|
||||
|
||||
@ -59,6 +59,74 @@ export function normalizeMessage(message: unknown): NormalizedMessage {
|
||||
return { role, content, timestamp, id };
|
||||
}
|
||||
|
||||
type SplitSystemMessageResult = {
|
||||
systemMessage: NormalizedMessage | null;
|
||||
message: NormalizedMessage;
|
||||
};
|
||||
|
||||
const SYSTEM_LINE_PREFIX = "System: ";
|
||||
|
||||
function parseSystemLine(line: string): string | null {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith(SYSTEM_LINE_PREFIX)) return null;
|
||||
const payload = trimmed.slice(SYSTEM_LINE_PREFIX.length).trim();
|
||||
return payload ? payload : null;
|
||||
}
|
||||
|
||||
export function splitSystemPreface(
|
||||
message: NormalizedMessage,
|
||||
): SplitSystemMessageResult {
|
||||
if (message.role.toLowerCase() !== "user") {
|
||||
return { systemMessage: null, message };
|
||||
}
|
||||
|
||||
const textParts = message.content
|
||||
.filter((item) => item.type === "text" && typeof item.text === "string")
|
||||
.map((item) => item.text ?? "")
|
||||
.filter((text) => text.trim().length > 0);
|
||||
|
||||
if (textParts.length === 0) return { systemMessage: null, message };
|
||||
|
||||
const joined = textParts.join("\n").trim();
|
||||
if (!joined.startsWith(SYSTEM_LINE_PREFIX)) {
|
||||
return { systemMessage: null, message };
|
||||
}
|
||||
|
||||
const lines = joined.split(/\r?\n/);
|
||||
const systemLines: string[] = [];
|
||||
let idx = 0;
|
||||
while (idx < lines.length) {
|
||||
const parsed = parseSystemLine(lines[idx] ?? "");
|
||||
if (!parsed) break;
|
||||
systemLines.push(parsed);
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
if (systemLines.length === 0) return { systemMessage: null, message };
|
||||
|
||||
// Skip blank separator line after system block, if present.
|
||||
while (idx < lines.length && !lines[idx]?.trim()) idx += 1;
|
||||
const remainder = lines.slice(idx).join("\n").trim();
|
||||
|
||||
const systemMessage: NormalizedMessage = {
|
||||
role: "system",
|
||||
content: [{ type: "text", text: systemLines.join("\n") }],
|
||||
timestamp: message.timestamp,
|
||||
};
|
||||
|
||||
if (!remainder) {
|
||||
return { systemMessage, message: { ...message, content: [] } };
|
||||
}
|
||||
|
||||
return {
|
||||
systemMessage,
|
||||
message: {
|
||||
...message,
|
||||
content: [{ type: "text", text: remainder }],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize role for grouping purposes.
|
||||
*/
|
||||
|
||||
@ -7,6 +7,7 @@ import { icons } from "../icons";
|
||||
import {
|
||||
normalizeMessage,
|
||||
normalizeRoleForGrouping,
|
||||
splitSystemPreface,
|
||||
} from "../chat/message-normalizer";
|
||||
import {
|
||||
renderMessageGroup,
|
||||
@ -337,10 +338,23 @@ function buildChatItems(props: ChatProps): Array<ChatItem | MessageGroup> {
|
||||
continue;
|
||||
}
|
||||
|
||||
const split = splitSystemPreface(normalized);
|
||||
if (split.systemMessage) {
|
||||
items.push({
|
||||
kind: "message",
|
||||
key: `system:${messageKey(msg, i)}`,
|
||||
message: split.systemMessage,
|
||||
});
|
||||
}
|
||||
|
||||
if (split.systemMessage && split.message.content.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
items.push({
|
||||
kind: "message",
|
||||
key: messageKey(msg, i),
|
||||
message: msg,
|
||||
message: split.systemMessage ? split.message : msg,
|
||||
});
|
||||
}
|
||||
if (props.showThinking) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user