feat(ui): implement chat completion views (#2201)

# What does this PR do?
 Implements table and detail views for chat completions

<img width="1548" alt="image"
src="https://github.com/user-attachments/assets/01061b7f-0d47-4b3b-b5ac-2df8f9035ef6"
/>
<img width="1549" alt="image"
src="https://github.com/user-attachments/assets/738d8612-8258-4c2c-858b-bee39030649f"
/>


## Test Plan
npm run test
This commit is contained in:
ehhuang 2025-05-22 22:05:54 -07:00 committed by GitHub
parent d8c6ab9bfc
commit 2708312168
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 6729 additions and 38 deletions

View file

@ -0,0 +1,193 @@
import {
extractTextFromContentPart,
extractDisplayableText,
} from "./format-message-content";
import { ChatMessage } from "@/lib/types";
describe("extractTextFromContentPart", () => {
it("should return an empty string for null or undefined input", () => {
expect(extractTextFromContentPart(null)).toBe("");
expect(extractTextFromContentPart(undefined)).toBe("");
});
it("should return the string itself if input is a string", () => {
expect(extractTextFromContentPart("Hello, world!")).toBe("Hello, world!");
expect(extractTextFromContentPart("")).toBe("");
});
it("should extract text from an array of text content objects", () => {
const content = [{ type: "text", text: "Which planet do humans live on?" }];
expect(extractTextFromContentPart(content)).toBe(
"Which planet do humans live on?",
);
});
it("should join text from multiple text content objects in an array", () => {
const content = [
{ type: "text", text: "Hello," },
{ type: "text", text: "world!" },
];
expect(extractTextFromContentPart(content)).toBe("Hello, world!");
});
it("should handle mixed text and image_url types in an array", () => {
const content = [
{ type: "text", text: "Look at this:" },
{ type: "image_url", image_url: { url: "http://example.com/image.png" } },
{ type: "text", text: "It's an image." },
];
expect(extractTextFromContentPart(content)).toBe(
"Look at this: [Image] It's an image.",
);
});
it("should return '[Image]' for an array with only an image_url object", () => {
const content = [
{ type: "image_url", image_url: { url: "http://example.com/image.png" } },
];
expect(extractTextFromContentPart(content)).toBe("[Image]");
});
it("should return an empty string for an empty array", () => {
expect(extractTextFromContentPart([])).toBe("");
});
it("should handle arrays with plain strings", () => {
const content = ["This is", " a test."] as any;
expect(extractTextFromContentPart(content)).toBe("This is a test.");
});
it("should filter out malformed or unrecognized objects in an array", () => {
const content = [
{ type: "text", text: "Valid" },
{ type: "unknown" },
{ text: "Missing type" },
null,
undefined,
{ type: "text", noTextProperty: true },
] as any;
expect(extractTextFromContentPart(content)).toBe("Valid");
});
it("should handle an array of mixed valid items and plain strings", () => {
const content = [
{ type: "text", text: "First part." },
"Just a string.",
{ type: "image_url", image_url: { url: "http://example.com/image.png" } },
{ type: "text", text: "Last part." },
] as any;
expect(extractTextFromContentPart(content)).toBe(
"First part. Just a string. [Image] Last part.",
);
});
});
describe("extractDisplayableText (composite function)", () => {
const mockFormatToolCallToString = (toolCall: any) => {
if (!toolCall || !toolCall.function || !toolCall.function.name) return "";
const args = toolCall.function.arguments
? JSON.stringify(toolCall.function.arguments)
: "";
return `${toolCall.function.name}(${args})`;
};
it("should return empty string for null or undefined message", () => {
expect(extractDisplayableText(null)).toBe("");
expect(extractDisplayableText(undefined)).toBe("");
});
it("should return only content part if no tool calls", () => {
const message: ChatMessage = {
role: "assistant",
content: "Hello there!",
};
expect(extractDisplayableText(message)).toBe("Hello there!");
});
it("should return only content part for complex content if no tool calls", () => {
const message: ChatMessage = {
role: "user",
content: [
{ type: "text", text: "Part 1" },
{ type: "text", text: "Part 2" },
],
};
expect(extractDisplayableText(message)).toBe("Part 1 Part 2");
});
it("should return only formatted tool call if content is empty or null", () => {
const toolCall = {
function: { name: "search", arguments: { query: "cats" } },
};
const messageWithEffectivelyEmptyContent: ChatMessage = {
role: "assistant",
content: "",
tool_calls: [toolCall],
};
expect(extractDisplayableText(messageWithEffectivelyEmptyContent)).toBe(
mockFormatToolCallToString(toolCall),
);
const messageWithEmptyContent: ChatMessage = {
role: "assistant",
content: "",
tool_calls: [toolCall],
};
expect(extractDisplayableText(messageWithEmptyContent)).toBe(
mockFormatToolCallToString(toolCall),
);
});
it("should combine content and formatted tool call", () => {
const toolCall = {
function: { name: "calculator", arguments: { expr: "2+2" } },
};
const message: ChatMessage = {
role: "assistant",
content: "The result is:",
tool_calls: [toolCall],
};
const expectedToolCallStr = mockFormatToolCallToString(toolCall);
expect(extractDisplayableText(message)).toBe(
`The result is: ${expectedToolCallStr}`,
);
});
it("should handle message with content an array and a tool call", () => {
const toolCall = {
function: { name: "get_weather", arguments: { city: "London" } },
};
const message: ChatMessage = {
role: "assistant",
content: [
{ type: "text", text: "Okay, checking weather for" },
{ type: "text", text: "London." },
],
tool_calls: [toolCall],
};
const expectedToolCallStr = mockFormatToolCallToString(toolCall);
expect(extractDisplayableText(message)).toBe(
`Okay, checking weather for London. ${expectedToolCallStr}`,
);
});
it("should return only content if tool_calls array is empty or undefined", () => {
const messageEmptyToolCalls: ChatMessage = {
role: "assistant",
content: "No tools here.",
tool_calls: [],
};
expect(extractDisplayableText(messageEmptyToolCalls)).toBe(
"No tools here.",
);
const messageUndefinedToolCalls: ChatMessage = {
role: "assistant",
content: "Still no tools.",
tool_calls: undefined,
};
expect(extractDisplayableText(messageUndefinedToolCalls)).toBe(
"Still no tools.",
);
});
});

View file

@ -0,0 +1,61 @@
import { ChatMessage, ChatMessageContentPart } from "@/lib/types";
import { formatToolCallToString } from "@/lib/format-tool-call";
export function extractTextFromContentPart(
content: string | ChatMessageContentPart[] | null | undefined,
): string {
if (content === null || content === undefined) {
return "";
}
if (typeof content === "string") {
return content;
} else if (Array.isArray(content)) {
const parts: string[] = [];
for (const item of content) {
if (
item &&
typeof item === "object" &&
item.type === "text" &&
typeof item.text === "string"
) {
parts.push(item.text);
} else if (
item &&
typeof item === "object" &&
item.type === "image_url"
) {
parts.push("[Image]"); // Placeholder for images
} else if (typeof item === "string") {
// Handle cases where an array might contain plain strings
parts.push(item);
}
}
return parts.join(" ");
} else {
return content;
}
}
export function extractDisplayableText(
message: ChatMessage | undefined | null,
): string {
if (!message) {
return "";
}
let textPart = extractTextFromContentPart(message.content);
let toolCallPart = "";
if (message.tool_calls && message.tool_calls.length > 0) {
// For summary, usually the first tool call is sufficient
toolCallPart = formatToolCallToString(message.tool_calls[0]);
}
if (textPart && toolCallPart) {
return `${textPart} ${toolCallPart}`;
} else if (toolCallPart) {
return toolCallPart;
} else {
return textPart; // textPart will be "" if message.content was initially null/undefined/empty array etc.
}
}

View file

@ -0,0 +1,33 @@
/**
* Formats a tool_call object into a string representation.
* Example: "functionName(argumentsString)"
* @param toolCall The tool_call object, expected to have a `function` property
* with `name` and `arguments`.
* @returns A formatted string or an empty string if data is malformed.
*/
export function formatToolCallToString(toolCall: any): string {
if (
!toolCall ||
!toolCall.function ||
typeof toolCall.function.name !== "string" ||
toolCall.function.arguments === undefined
) {
return "";
}
const name = toolCall.function.name;
const args = toolCall.function.arguments;
let argsString: string;
if (typeof args === "string") {
argsString = args;
} else {
try {
argsString = JSON.stringify(args);
} catch (error) {
return "";
}
}
return `${name}(${argsString})`;
}

View file

@ -0,0 +1,8 @@
export function truncateText(
text: string | null | undefined,
maxLength: number = 50,
): string {
if (!text) return "N/A";
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + "...";
}

View file

@ -0,0 +1,44 @@
export interface TextContentBlock {
type: "text";
text: string;
}
export interface ImageUrlDetail {
url: string;
detail?: "low" | "high" | "auto";
}
export interface ImageUrlContentBlock {
type: "image_url";
// Support both simple URL string and detailed object, though our parser currently just looks for type: "image_url"
image_url: string | ImageUrlDetail;
}
// Union of known content part types. Add more specific types as needed.
export type ChatMessageContentPart =
| TextContentBlock
| ImageUrlContentBlock
| { type: string; [key: string]: any }; // Fallback for other potential types
export interface ChatMessage {
role: string;
content: string | ChatMessageContentPart[]; // Updated content type
name?: string | null;
tool_calls?: any | null; // This could also be refined to a more specific ToolCall[] type
}
export interface Choice {
message: ChatMessage;
finish_reason: string;
index: number;
logprobs?: any | null;
}
export interface ChatCompletion {
id: string;
choices: Choice[];
object: string;
created: number;
model: string;
input_messages: ChatMessage[];
}