@langgraph-js/sdk
Version:
The UI SDK for LangGraph - seamlessly integrate your AI agents with frontend interfaces
130 lines (117 loc) • 4.71 kB
text/typescript
import { atom, computed, effect, PreinitializedWritableAtom } from "nanostores";
export * from "./types.js";
import { ArtifactCommand } from "./types.js";
import { ToolRenderData } from "../tool/ToolUI.js";
import { LangGraphClient, RenderMessage } from "../LangGraphClient.js";
export interface ComposedArtifact {
id: string;
filename: string;
filetype: string;
versions: Artifact[];
}
export interface Artifact {
group_id: string;
id: string;
code: string;
filename: string;
filetype: string;
version: number;
is_done: boolean;
}
// 创建 artifacts computed store
const extractArtifactsFromMessages = (renderMessages: any[], client: any): ComposedArtifact[] => {
type MiddleArtifactCommand = ArtifactCommand & {
tool_id?: string;
is_done?: boolean;
};
const files = new Map<string, MiddleArtifactCommand[]>();
for (const message of renderMessages) {
if (message.type === "tool" && message.name === "create_artifacts") {
const tool = new ToolRenderData<ArtifactCommand, {}>(message, client!);
const command = tool.getInputRepaired() as MiddleArtifactCommand;
if (!command.id) continue;
command.tool_id = tool.message.id!;
command.is_done = tool.state === "done";
files.set(command.id, [...(files.get(command.id) || []), command]);
}
}
const composedFiles = new Map<string, Artifact[]>();
// 遍历每个 ID 的命令序列,生成对应的 artifact 版本
for (const [id, commands] of files) {
const artifacts: Artifact[] = [];
let currentContent = "";
let currentFilename = "";
let currentFiletype = "";
let version = 1;
// 按命令顺序处理每个操作
for (const command of commands) {
switch (command.command) {
case "create":
// 创建新 artifact,直接使用 content
currentContent = command.content;
currentFilename = command.title || `artifact-${id}`;
currentFiletype = command.type || command.language;
break;
case "update":
// 更新现有内容,使用 old_str 和 new_str 进行替换
if (command.old_str && command.new_str) {
currentContent = currentContent.replace(command.old_str, command.new_str);
} else if (command.content) {
// 如果没有 old_str/new_str,则直接使用 content 覆盖
currentContent = command.content;
}
break;
case "rewrite":
currentContent = command.content;
break;
}
// 创建当前版本的 artifact
const artifact: Artifact = {
group_id: id,
id: command.tool_id!,
code: currentContent,
filename: currentFilename,
filetype: currentFiletype,
version: version,
is_done: command.is_done!,
};
artifacts.push(artifact);
version++;
}
composedFiles.set(id, artifacts);
}
return [...composedFiles.values()].map((artifacts) => ({
id: artifacts[0].group_id,
filename: artifacts[artifacts.length - 1].filename,
filetype: artifacts[artifacts.length - 1].filetype,
versions: artifacts,
}));
};
export const useArtifacts = (renderMessages: PreinitializedWritableAtom<RenderMessage[]>, client: PreinitializedWritableAtom<LangGraphClient<unknown> | null>) => {
// 创建 artifacts store
const showArtifact = atom<boolean>(false);
const currentArtifactId = atom<[string, string] | null>(null);
const artifacts = atom<ComposedArtifact[]>([]);
effect([renderMessages, client], () => {
artifacts.set(extractArtifactsFromMessages(renderMessages.get(), client.get()));
});
const debouncedSetCurrentArtifactById = (id: string, tool_id: string) => {
const current = currentArtifactId.get();
if (current?.[0] === id && current?.[1] === tool_id) {
return;
}
showArtifact.set(true);
currentArtifactId.set([id, tool_id]);
};
return {
data: {
artifacts,
currentArtifactId,
showArtifact,
},
mutation: {
setCurrentArtifactById: debouncedSetCurrentArtifactById,
setShowArtifact: (show: boolean) => showArtifact.set(show),
},
};
};