# Conflicts: # frontend-web/src/components/workspace/messages/message-list-item.tsx # offline-backend-20260512/backend/app/gateway/app.py # offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/html_page.py # offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/service.py
95 lines
2.8 KiB
TypeScript
95 lines
2.8 KiB
TypeScript
import { useContext } from "react";
|
|
import type { Message } from "@langchain/langgraph-sdk";
|
|
|
|
import { RouteBaseContext } from "@/core/route-base";
|
|
import type { AgentThread, AgentThreadContext } from "./types";
|
|
|
|
type ThreadAgentRef = Pick<AgentThreadContext, "agent_id" | "agent_name">;
|
|
|
|
type ThreadRouteTarget =
|
|
| string
|
|
| {
|
|
thread_id: string;
|
|
context?: ThreadAgentRef | null;
|
|
metadata?: Record<string, unknown> | null;
|
|
};
|
|
|
|
function pickAgentRef(
|
|
context: ThreadAgentRef | null | undefined,
|
|
metadata: Record<string, unknown> | null | undefined,
|
|
): string | undefined {
|
|
if (context?.agent_id) return context.agent_id;
|
|
if (context?.agent_name) return context.agent_name;
|
|
const metaId = metadata?.agent_id;
|
|
if (typeof metaId === "string" && metaId) return metaId;
|
|
const metaName = metadata?.agent_name;
|
|
if (typeof metaName === "string" && metaName) return metaName;
|
|
return undefined;
|
|
}
|
|
|
|
export function pathOfThread(
|
|
thread: ThreadRouteTarget,
|
|
context?: ThreadAgentRef | null,
|
|
base = "/page/workspace",
|
|
) {
|
|
const threadId = typeof thread === "string" ? thread : thread.thread_id;
|
|
const agentRef =
|
|
typeof thread === "string"
|
|
? pickAgentRef(context, null)
|
|
: pickAgentRef(thread.context, thread.metadata);
|
|
|
|
return agentRef
|
|
? `${base}/agents/${encodeURIComponent(agentRef)}/chats/${threadId}`
|
|
: `${base}/chats/${threadId}`;
|
|
}
|
|
|
|
export function usePathOfThread() {
|
|
const base = useContext(RouteBaseContext);
|
|
return (thread: ThreadRouteTarget, context?: ThreadAgentRef | null) =>
|
|
pathOfThread(thread, context, base);
|
|
}
|
|
|
|
export function textOfMessage(message: Message) {
|
|
if (typeof message.content === "string") {
|
|
return message.content;
|
|
} else if (Array.isArray(message.content)) {
|
|
for (const part of message.content) {
|
|
if (part.type === "text") {
|
|
return part.text;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function titleOfThread(thread: AgentThread, fallback = "Untitled") {
|
|
const title = thread.values?.title;
|
|
if (title) return title;
|
|
|
|
// No title yet (e.g. the very first turn is still streaming, or the backend
|
|
// title pass hasn't finished). Derive a sensible display title from the first
|
|
// user message so the user never sees a bare "Untitled" placeholder.
|
|
const messages = thread.values?.messages;
|
|
if (Array.isArray(messages)) {
|
|
const firstHuman = messages.find((m) => m.type === "human");
|
|
const text = firstHuman ? textOfMessage(firstHuman)?.trim() : null;
|
|
if (text) {
|
|
return text.length > 50 ? `${text.slice(0, 50).trimEnd()}…` : text;
|
|
}
|
|
}
|
|
|
|
return fallback;
|
|
}
|
|
|
|
export function sanitizeThreadTitleForDisplay(title: string | null | undefined) {
|
|
if (!title) return "Untitled";
|
|
|
|
const cleaned = title
|
|
.replace(/<\/?[^>\n]+>/g, " ")
|
|
.replace(/<[^>\n]*$/g, " ")
|
|
.replace(/\s+/g, " ")
|
|
.trim();
|
|
|
|
return cleaned || "Untitled";
|
|
}
|