front end update:

- chat support markdown
- stop button for chat
- configurable for deepagent
This commit is contained in:
2026-03-07 14:50:01 +08:00
parent 3932d695bf
commit ac99dfd56b
4 changed files with 150 additions and 16 deletions

View File

@@ -39,6 +39,7 @@ type EditableAgent = {
prompts: Record<string, string>; prompts: Record<string, string>;
apiKey: string; apiKey: string;
llmName: string; llmName: string;
actBackend: DeepAgentActBackend;
}; };
type AgentChatMessage = { type AgentChatMessage = {
@@ -48,6 +49,7 @@ type AgentChatMessage = {
}; };
type ActiveTab = "agents" | "discussions" | "mcp"; type ActiveTab = "agents" | "discussions" | "mcp";
type DeepAgentActBackend = "state_bk" | "local_shell" | "daytona_sandbox";
type McpTransport = "streamable_http" | "sse" | "stdio"; type McpTransport = "streamable_http" | "sse" | "stdio";
type McpEntry = { type McpEntry = {
id: string; id: string;
@@ -62,6 +64,15 @@ type McpEntry = {
const DEFAULT_LLM_NAME = "qwen-plus"; const DEFAULT_LLM_NAME = "qwen-plus";
const DEFAULT_API_KEY = ""; const DEFAULT_API_KEY = "";
const DEFAULT_DEEPAGENT_ACT_BACKEND: DeepAgentActBackend = "state_bk";
const DEEPAGENT_BACKEND_OPTIONS: Array<{
value: DeepAgentActBackend;
label: string;
}> = [
{ value: "state_bk", label: "state_bk" },
{ value: "local_shell", label: "local_shell" },
{ value: "daytona_sandbox", label: "daytona_sandbox" },
];
const LOCAL_DASHSCOPE_BASE = "http://127.0.0.1:8500/v1/apps"; const LOCAL_DASHSCOPE_BASE = "http://127.0.0.1:8500/v1/apps";
const MCP_TRANSPORT_OPTIONS: McpTransport[] = ["streamable_http", "sse", "stdio"]; const MCP_TRANSPORT_OPTIONS: McpTransport[] = ["streamable_http", "sse", "stdio"];
const GRAPH_ARCH_IMAGE_MODULES = import.meta.glob( const GRAPH_ARCH_IMAGE_MODULES = import.meta.glob(
@@ -74,6 +85,9 @@ const FALLBACK_PROMPTS_BY_GRAPH: Record<string, Record<string, string>> = {
chat_prompt: "", chat_prompt: "",
tool_prompt: "", tool_prompt: "",
}, },
deepagent: {
sys_prompt: "",
},
react: { react: {
sys_prompt: "", sys_prompt: "",
}, },
@@ -412,6 +426,26 @@ function createConversationId(): string {
return `conv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; return `conv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
} }
function normalizeDeepAgentActBackend(value: unknown): DeepAgentActBackend {
if (value === "local_shell" || value === "localshell") {
return "local_shell";
}
if (value === "daytona_sandbox" || value === "daytonasandbox") {
return "daytona_sandbox";
}
if (value === "state_bk" || value === "statebk") {
return "state_bk";
}
return DEFAULT_DEEPAGENT_ACT_BACKEND;
}
function buildGraphParams(editor: EditableAgent): Record<string, unknown> {
if (editor.graphId === "deepagent") {
return { act_bkend: editor.actBackend };
}
return {};
}
function toEditable( function toEditable(
config: GraphConfigReadResponse, config: GraphConfigReadResponse,
draft: boolean draft: boolean
@@ -428,6 +462,7 @@ function toEditable(
prompts: config.prompt_dict || {}, prompts: config.prompt_dict || {},
apiKey: config.api_key || DEFAULT_API_KEY, apiKey: config.api_key || DEFAULT_API_KEY,
llmName: DEFAULT_LLM_NAME, llmName: DEFAULT_LLM_NAME,
actBackend: DEFAULT_DEEPAGENT_ACT_BACKEND,
}; };
} }
@@ -455,6 +490,7 @@ export default function App() {
const [chatInput, setChatInput] = useState<string>(""); const [chatInput, setChatInput] = useState<string>("");
const [chatMessages, setChatMessages] = useState<AgentChatMessage[]>([]); const [chatMessages, setChatMessages] = useState<AgentChatMessage[]>([]);
const [chatSending, setChatSending] = useState(false); const [chatSending, setChatSending] = useState(false);
const [chatAbortController, setChatAbortController] = useState<AbortController | null>(null);
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const configKeySet = useMemo( const configKeySet = useMemo(
@@ -714,6 +750,10 @@ export default function App() {
graphId, graphId,
prompts: { ...defaults.prompt_dict }, prompts: { ...defaults.prompt_dict },
toolKeys: defaults.tool_keys || [], toolKeys: defaults.tool_keys || [],
actBackend:
graphId === "deepagent"
? prev.actBackend || DEFAULT_DEEPAGENT_ACT_BACKEND
: DEFAULT_DEEPAGENT_ACT_BACKEND,
}; };
if (next.isDraft) { if (next.isDraft) {
setDraftAgents((drafts) => drafts.map((draft) => (draft.id === next.id ? next : draft))); setDraftAgents((drafts) => drafts.map((draft) => (draft.id === next.id ? next : draft)));
@@ -905,9 +945,8 @@ export default function App() {
const active = await getPipelineDefaultConfig(editor.pipelineId.trim()); const active = await getPipelineDefaultConfig(editor.pipelineId.trim());
targetPromptSetId = active.prompt_set_id; targetPromptSetId = active.prompt_set_id;
} catch { } catch {
throw new Error( // For a brand-new pipeline, let backend create a new prompt set.
"No active prompt set for this pipeline. Create/activate one via backend first." targetPromptSetId = undefined;
);
} }
} }
const upsertResp = await upsertGraphConfig({ const upsertResp = await upsertGraphConfig({
@@ -929,6 +968,7 @@ export default function App() {
api_key: editor.apiKey.trim(), api_key: editor.apiKey.trim(),
llm_name: editor.llmName || DEFAULT_LLM_NAME, llm_name: editor.llmName || DEFAULT_LLM_NAME,
enabled: isEditorRunning, enabled: isEditorRunning,
graph_params: buildGraphParams(editor),
}); });
await refreshRunning(); await refreshRunning();
} catch (error) { } catch (error) {
@@ -1012,6 +1052,7 @@ export default function App() {
api_key: editor.apiKey.trim(), api_key: editor.apiKey.trim(),
llm_name: editor.llmName, llm_name: editor.llmName,
enabled: true, enabled: true,
graph_params: buildGraphParams(editor),
}); });
await refreshRunning(); await refreshRunning();
if (resp.reload_required) { if (resp.reload_required) {
@@ -1081,6 +1122,9 @@ export default function App() {
} }
const authKey = runtimeFastApiKey.trim() || "dev-key"; const authKey = runtimeFastApiKey.trim() || "dev-key";
const controller = new AbortController();
setChatAbortController(controller);
const userMessage: AgentChatMessage = { const userMessage: AgentChatMessage = {
id: `user-${Date.now()}`, id: `user-${Date.now()}`,
role: "user", role: "user",
@@ -1104,6 +1148,7 @@ export default function App() {
sessionId: chatConversationId, sessionId: chatConversationId,
apiKey: authKey, apiKey: authKey,
message, message,
signal: controller.signal,
onText: (text) => { onText: (text) => {
setChatMessages((prev) => setChatMessages((prev) =>
prev.map((item) => prev.map((item) =>
@@ -1118,6 +1163,19 @@ export default function App() {
}, },
}); });
} catch (error) { } catch (error) {
if ((error as Error).message === "Request cancelled") {
setChatMessages((prev) =>
prev.map((item) =>
item.id === assistantMessageId
? {
...item,
content: item.content + "\n\n*[Stopped]*",
}
: item
)
);
return;
}
const messageText = (error as Error).message || "Chat request failed."; const messageText = (error as Error).message || "Chat request failed.";
setStatusMessage(messageText); setStatusMessage(messageText);
setChatMessages((prev) => setChatMessages((prev) =>
@@ -1132,6 +1190,7 @@ export default function App() {
); );
} finally { } finally {
setChatSending(false); setChatSending(false);
setChatAbortController(null);
} }
} }
@@ -1337,6 +1396,28 @@ export default function App() {
/> />
</label> </label>
{editor.graphId === "deepagent" ? (
<label>
act_bkend
<select
value={editor.actBackend}
onChange={(e) =>
updateEditor(
"actBackend",
normalizeDeepAgentActBackend(e.target.value)
)
}
disabled={busy}
>
{DEEPAGENT_BACKEND_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
) : null}
<div className="prompt-section"> <div className="prompt-section">
<h3>Prompts</h3> <h3>Prompts</h3>
{Object.keys(editor.prompts).length === 0 ? ( {Object.keys(editor.prompts).length === 0 ? (
@@ -1670,7 +1751,11 @@ export default function App() {
className={`chat-modal-message ${message.role === "assistant" ? "assistant" : "user"}`} className={`chat-modal-message ${message.role === "assistant" ? "assistant" : "user"}`}
> >
<strong>{message.role === "assistant" ? "Agent" : "You"}</strong> <strong>{message.role === "assistant" ? "Agent" : "You"}</strong>
<p>{message.content || (chatSending && message.role === "assistant" ? "..." : "")}</p> <div className="chat-message-content">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{message.content || (chatSending && message.role === "assistant" ? "..." : "")}
</ReactMarkdown>
</div>
</article> </article>
)) ))
)} )}
@@ -1679,19 +1764,41 @@ export default function App() {
<textarea <textarea
value={chatInput} value={chatInput}
onChange={(event) => setChatInput(event.target.value)} onChange={(event) => setChatInput(event.target.value)}
placeholder="Type your message..." onKeyDown={(event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
if (chatInput.trim() && !chatSending) {
sendAgentChatMessage().catch(() => undefined);
}
}
}}
placeholder="Type your message... (Enter to send, Shift+Enter for new line)"
rows={3} rows={3}
disabled={chatSending} disabled={chatSending}
/> />
<button <div className="chat-modal-actions">
type="button" {chatSending ? (
onClick={() => { <button
sendAgentChatMessage().catch(() => undefined); type="button"
}} className="chat-stop-button"
disabled={chatSending || !chatInput.trim()} onClick={() => {
> chatAbortController?.abort();
Send (Stream) }}
</button> >
Stop
</button>
) : (
<button
type="button"
onClick={() => {
sendAgentChatMessage().catch(() => undefined);
}}
disabled={chatSending || !chatInput.trim()}
>
Send (Stream)
</button>
)}
</div>
</div> </div>
</section> </section>
</div> </div>

View File

@@ -173,6 +173,7 @@ type StreamAgentChatOptions = {
apiKey: string; apiKey: string;
message: string; message: string;
onText: (text: string) => void; onText: (text: string) => void;
signal?: AbortSignal;
}; };
function parseErrorDetail(payload: unknown): string | null { function parseErrorDetail(payload: unknown): string | null {
@@ -186,7 +187,7 @@ function parseErrorDetail(payload: unknown): string | null {
export async function streamAgentChatResponse( export async function streamAgentChatResponse(
options: StreamAgentChatOptions options: StreamAgentChatOptions
): Promise<string> { ): Promise<string> {
const { appId, sessionId, apiKey, message, onText } = options; const { appId, sessionId, apiKey, message, onText, signal } = options;
const response = await fetch( const response = await fetch(
`${API_BASE_URL}/v1/apps/${encodeURIComponent(appId)}/sessions/${encodeURIComponent(sessionId)}/responses`, `${API_BASE_URL}/v1/apps/${encodeURIComponent(appId)}/sessions/${encodeURIComponent(sessionId)}/responses`,
{ {
@@ -199,6 +200,7 @@ export async function streamAgentChatResponse(
messages: [{ role: "user", content: message }], messages: [{ role: "user", content: message }],
stream: true, stream: true,
}), }),
signal,
} }
); );
@@ -226,6 +228,10 @@ export async function streamAgentChatResponse(
let latestText = ""; let latestText = "";
while (true) { while (true) {
if (signal?.aborted) {
reader.cancel();
throw new Error("Request cancelled");
}
const { value, done } = await reader.read(); const { value, done } = await reader.read();
if (done) { if (done) {
break; break;

View File

@@ -618,6 +618,7 @@ button:disabled {
display: grid; display: grid;
gap: 8px; gap: 8px;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
align-items: start;
} }
.chat-modal-input textarea { .chat-modal-input textarea {
@@ -628,3 +629,23 @@ button:disabled {
resize: vertical; resize: vertical;
} }
.chat-modal-actions {
display: flex;
gap: 8px;
height: 100%;
}
.chat-modal-actions button {
height: auto;
white-space: nowrap;
}
.chat-stop-button {
background-color: #dc3545;
color: white;
}
.chat-stop-button:hover {
background-color: #bb2d3b;
}

View File

@@ -55,6 +55,7 @@ export type PipelineCreateRequest = {
api_key?: string; api_key?: string;
llm_name: string; llm_name: string;
enabled?: boolean; enabled?: boolean;
graph_params?: Record<string, unknown>;
}; };
export type PipelineSpec = { export type PipelineSpec = {
@@ -63,7 +64,6 @@ export type PipelineSpec = {
enabled: boolean; enabled: boolean;
config_file: string; config_file: string;
llm_name: string; llm_name: string;
overrides: Record<string, unknown>;
}; };
export type PipelineCreateResponse = { export type PipelineCreateResponse = {