refactor: refine folder structure and rename

This commit is contained in:
Henry Li
2026-01-16 09:13:02 +08:00
parent 61499624a0
commit e9846c1dda
13 changed files with 65 additions and 37 deletions

View File

@@ -1,9 +1,11 @@
import type { ChatStatus } from "ai";
import { LightbulbIcon, LightbulbOffIcon } from "lucide-react";
import { useCallback, type ComponentProps } from "react";
import {
PromptInput,
PromptInputBody,
PromptInputButton,
PromptInputFooter,
PromptInputSubmit,
PromptInputTextarea,
@@ -11,6 +13,8 @@ import {
} from "@/components/ai-elements/prompt-input";
import { cn } from "@/lib/utils";
import { Tooltip } from "./tooltip";
export function InputBox({
className,
autoFocus,
@@ -57,7 +61,13 @@ export function InputBox({
/>
</PromptInputBody>
<PromptInputFooter className="flex">
<div></div>
<div>
<Tooltip content="">
<PromptInputButton>
<LightbulbOffIcon className="size-4" />
</PromptInputButton>
</Tooltip>
</div>
<div className="flex items-center gap-2">
<PromptInputSubmit
className="rounded-full"

View File

@@ -21,14 +21,13 @@ import {
ChainOfThoughtStep,
} from "@/components/ai-elements/chain-of-thought";
import { MessageResponse } from "@/components/ai-elements/message";
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
import { extractTitleFromMarkdown } from "@/core/utils/markdown";
import { cn } from "@/lib/utils";
import {
extractReasoningContentFromMessage,
findToolCallResult,
} from "./utils";
} from "@/core/messages/utils";
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
import { extractTitleFromMarkdown } from "@/core/utils/markdown";
import { cn } from "@/lib/utils";
export function MessageGroup({
className,

View File

@@ -6,11 +6,15 @@ import {
MessageContent as AIElementMessageContent,
MessageResponse as AIElementMessageResponse,
} from "@/components/ai-elements/message";
import {
extractContentFromMessage,
hasReasoning,
hasToolCalls,
} from "@/core/messages/utils";
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
import { cn } from "@/lib/utils";
import { MessageGroup } from "./message-group";
import { extractContentFromMessage, hasReasoning, hasToolCalls } from "./utils";
export function MessageListItem({
className,

View File

@@ -5,7 +5,8 @@ import {
ConversationContent,
ConversationScrollButton,
} from "@/components/ai-elements/conversation";
import type { MessageThreadState } from "@/core/thread";
import { groupMessages, hasContent } from "@/core/messages/utils";
import type { AgentThreadState } from "@/core/threads";
import { cn } from "@/lib/utils";
import { StreamingIndicator } from "../streaming-indicator";
@@ -13,14 +14,13 @@ import { StreamingIndicator } from "../streaming-indicator";
import { MessageGroup } from "./message-group";
import { MessageListItem } from "./message-list-item";
import { MessageListSkeleton } from "./skeleton";
import { groupMessages, hasContent } from "./utils";
export function MessageList({
className,
thread,
}: {
className?: string;
thread: UseStream<MessageThreadState>;
thread: UseStream<AgentThreadState>;
}) {
if (thread.isThreadLoading) {
return <MessageListSkeleton />;

View File

@@ -1,157 +0,0 @@
import type { Message } from "@langchain/langgraph-sdk";
export function groupMessages<T>(
messages: Message[],
mapper: (
groupedMessages: Message[],
groupIndex: number,
isLastGroup: boolean,
) => T,
isLoading = false,
): T[] {
if (messages.length === 0) {
return [];
}
const resultsOfGroups: T[] = [];
let currentGroup: Message[] = [];
const lastMessage = messages[messages.length - 1]!;
const yieldCurrentGroup = () => {
if (currentGroup.length > 0) {
const resultOfGroup = mapper(
currentGroup,
resultsOfGroups.length,
currentGroup.includes(lastMessage),
);
if (resultOfGroup !== undefined && resultOfGroup !== null) {
resultsOfGroups.push(resultOfGroup);
}
currentGroup = [];
}
};
let messageIndex = 0;
for (const message of messages) {
if (message.type === "human") {
// Human messages are always shown as a individual group
yieldCurrentGroup();
currentGroup.push(message);
yieldCurrentGroup();
} else if (message.type === "tool") {
// Tool messages are always shown with the assistant messages that contains the tool calls
currentGroup.push(message);
} else if (message.type === "ai") {
if (
hasToolCalls(message) ||
(extractTextFromMessage(message) === "" &&
extractReasoningContentFromMessage(message) !== "" &&
messageIndex === messages.length - 1 &&
isLoading)
) {
// Assistant messages without any content are folded into the previous group
// Normally, these are tool calls (with or without thinking)
currentGroup.push(message);
} else {
// Assistant messages with content (text or images) are shown as a group if they have content
// No matter whether it has tool calls or not
yieldCurrentGroup();
currentGroup.push(message);
}
}
messageIndex++;
}
yieldCurrentGroup();
return resultsOfGroups;
}
export function extractTextFromMessage(message: Message) {
if (typeof message.content === "string") {
return message.content.trim();
}
if (Array.isArray(message.content)) {
return message.content
.map((content) => (content.type === "text" ? content.text : ""))
.join("\n")
.trim();
}
return "";
}
export function extractContentFromMessage(message: Message) {
if (typeof message.content === "string") {
return message.content.trim();
}
if (Array.isArray(message.content)) {
return message.content
.map((content) => {
switch (content.type) {
case "text":
return content.text;
case "image_url":
const imageURL = extractURLFromImageURLContent(content.image_url);
return `![image](${imageURL})`;
default:
return "";
}
})
.join("\n")
.trim();
}
return "";
}
export function extractReasoningContentFromMessage(message: Message) {
if (message.type !== "ai" || !message.additional_kwargs) {
return null;
}
if ("reasoning_content" in message.additional_kwargs) {
return message.additional_kwargs.reasoning_content as string | null;
}
return null;
}
export function extractURLFromImageURLContent(
content:
| string
| {
url: string;
},
) {
if (typeof content === "string") {
return content;
}
return content.url;
}
export function hasContent(message: Message) {
if (typeof message.content === "string") {
return message.content.trim().length > 0;
}
if (Array.isArray(message.content)) {
return message.content.length > 0;
}
return false;
}
export function hasReasoning(message: Message) {
return (
message.type === "ai" &&
typeof message.additional_kwargs?.reasoning_content === "string"
);
}
export function hasToolCalls(message: Message) {
return (
message.type === "ai" && message.tool_calls && message.tool_calls.length > 0
);
}
export function findToolCallResult(toolCallId: string, messages: Message[]) {
for (const message of messages) {
if (message.type === "tool" && message.tool_call_id === toolCallId) {
const content = extractTextFromMessage(message);
if (content) {
return content;
}
}
}
return undefined;
}

View File

@@ -21,7 +21,7 @@ import {
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { useDeleteThread, useThreads } from "@/core/api";
import { pathOfThread, titleOfThread } from "@/core/thread/utils";
import { pathOfThread, titleOfThread } from "@/core/threads/utils";
export function RecentChatList() {
const router = useRouter();