@alexanderolsen/create-deepagent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +661 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
- package/registry/frameworks/deno/.env.example +6 -0
- package/registry/frameworks/deno/README.md +137 -0
- package/registry/frameworks/deno/client/index.html +23 -0
- package/registry/frameworks/deno/client/package.json +30 -0
- package/registry/frameworks/deno/client/public/favicon.ico +0 -0
- package/registry/frameworks/deno/client/src/components/Chat.tsx +124 -0
- package/registry/frameworks/deno/client/src/components/ChatApp.tsx +129 -0
- package/registry/frameworks/deno/client/src/components/Conversation.tsx +91 -0
- package/registry/frameworks/deno/client/src/components/MessageBubbles.tsx +88 -0
- package/registry/frameworks/deno/client/src/components/MessageReasoning.tsx +71 -0
- package/registry/frameworks/deno/client/src/components/MessageThread.tsx +135 -0
- package/registry/frameworks/deno/client/src/components/StreamingIndicator.tsx +36 -0
- package/registry/frameworks/deno/client/src/components/Subagents.tsx +120 -0
- package/registry/frameworks/deno/client/src/components/ThemeIcons.tsx +31 -0
- package/registry/frameworks/deno/client/src/components/ThreadHistory.tsx +73 -0
- package/registry/frameworks/deno/client/src/components/ToolCall.tsx +89 -0
- package/registry/frameworks/deno/client/src/lib/agent-type.ts +4 -0
- package/registry/frameworks/deno/client/src/lib/chat/threads-client.ts +51 -0
- package/registry/frameworks/deno/client/src/main.tsx +11 -0
- package/registry/frameworks/deno/client/src/styles/globals.css +714 -0
- package/registry/frameworks/deno/client/src/vite-env.d.ts +1 -0
- package/registry/frameworks/deno/client/tsconfig.app.json +7 -0
- package/registry/frameworks/deno/client/tsconfig.json +24 -0
- package/registry/frameworks/deno/client/tsconfig.node.json +19 -0
- package/registry/frameworks/deno/client/vite.config.ts +24 -0
- package/registry/frameworks/deno/deno.json +16 -0
- package/registry/frameworks/deno/main.ts +23 -0
- package/registry/frameworks/deno/package.json +14 -0
- package/registry/frameworks/deno/server/agent/index.ts +60 -0
- package/registry/frameworks/deno/server/agent/middleware.ts +24 -0
- package/registry/frameworks/deno/server/agent/tools.ts +64 -0
- package/registry/frameworks/deno/server/registry.ts +40 -0
- package/registry/frameworks/deno/server/routes.ts +114 -0
- package/registry/frameworks/deno/server/serialize.ts +30 -0
- package/registry/frameworks/deno/server/session.ts +210 -0
- package/registry/frameworks/deno/server/threads.ts +404 -0
- package/registry/frameworks/deno.ts +17 -0
- package/registry/frameworks/hono/.env.example +6 -0
- package/registry/frameworks/hono/README.md +186 -0
- package/registry/frameworks/hono/index.html +22 -0
- package/registry/frameworks/hono/package.json +42 -0
- package/registry/frameworks/hono/src/components/Chat.tsx +124 -0
- package/registry/frameworks/hono/src/components/ChatApp.tsx +129 -0
- package/registry/frameworks/hono/src/components/Conversation.tsx +90 -0
- package/registry/frameworks/hono/src/components/MessageBubbles.tsx +88 -0
- package/registry/frameworks/hono/src/components/MessageReasoning.tsx +71 -0
- package/registry/frameworks/hono/src/components/MessageThread.tsx +135 -0
- package/registry/frameworks/hono/src/components/StreamingIndicator.tsx +36 -0
- package/registry/frameworks/hono/src/components/Subagents.tsx +120 -0
- package/registry/frameworks/hono/src/components/ThemeIcons.tsx +31 -0
- package/registry/frameworks/hono/src/components/ThreadHistory.tsx +73 -0
- package/registry/frameworks/hono/src/components/ToolCall.tsx +89 -0
- package/registry/frameworks/hono/src/lib/agent/types.ts +4 -0
- package/registry/frameworks/hono/src/lib/chat/threads-client.ts +57 -0
- package/registry/frameworks/hono/src/main.tsx +11 -0
- package/registry/frameworks/hono/src/styles/globals.css +714 -0
- package/registry/frameworks/hono/src/vite-env.d.ts +1 -0
- package/registry/frameworks/hono/tsconfig.app.json +22 -0
- package/registry/frameworks/hono/tsconfig.json +7 -0
- package/registry/frameworks/hono/tsconfig.worker.json +18 -0
- package/registry/frameworks/hono/vite.config.ts +16 -0
- package/registry/frameworks/hono/worker/agent/index.ts +53 -0
- package/registry/frameworks/hono/worker/agent/middleware.ts +20 -0
- package/registry/frameworks/hono/worker/agent/tools.ts +55 -0
- package/registry/frameworks/hono/worker/durable-objects/thread-session.ts +159 -0
- package/registry/frameworks/hono/worker/env.d.ts +17 -0
- package/registry/frameworks/hono/worker/index.ts +140 -0
- package/registry/frameworks/hono/worker/server/registry.ts +39 -0
- package/registry/frameworks/hono/worker/server/runs.ts +82 -0
- package/registry/frameworks/hono/worker/server/serialize.ts +30 -0
- package/registry/frameworks/hono/worker/server/threads.ts +404 -0
- package/registry/frameworks/hono/worker/tsconfig.json +4 -0
- package/registry/frameworks/hono/wrangler.jsonc +28 -0
- package/registry/frameworks/hono.ts +35 -0
- package/registry/frameworks/next/.env.example +6 -0
- package/registry/frameworks/next/README.md +173 -0
- package/registry/frameworks/next/app/api/threads/[threadId]/commands/route.ts +21 -0
- package/registry/frameworks/next/app/api/threads/[threadId]/history/route.ts +35 -0
- package/registry/frameworks/next/app/api/threads/[threadId]/route.ts +13 -0
- package/registry/frameworks/next/app/api/threads/[threadId]/state/route.ts +51 -0
- package/registry/frameworks/next/app/api/threads/[threadId]/stream/route.ts +30 -0
- package/registry/frameworks/next/app/api/threads/route.ts +11 -0
- package/registry/frameworks/next/app/favicon.ico +0 -0
- package/registry/frameworks/next/app/globals.css +712 -0
- package/registry/frameworks/next/app/layout.tsx +34 -0
- package/registry/frameworks/next/app/page.tsx +5 -0
- package/registry/frameworks/next/components/Chat.tsx +124 -0
- package/registry/frameworks/next/components/ChatApp.tsx +129 -0
- package/registry/frameworks/next/components/Conversation.tsx +90 -0
- package/registry/frameworks/next/components/MessageBubbles.tsx +88 -0
- package/registry/frameworks/next/components/MessageReasoning.tsx +71 -0
- package/registry/frameworks/next/components/MessageThread.tsx +135 -0
- package/registry/frameworks/next/components/StreamingIndicator.tsx +36 -0
- package/registry/frameworks/next/components/Subagents.tsx +120 -0
- package/registry/frameworks/next/components/ThemeIcons.tsx +31 -0
- package/registry/frameworks/next/components/ThreadHistory.tsx +73 -0
- package/registry/frameworks/next/components/ToolCall.tsx +89 -0
- package/registry/frameworks/next/eslint.config.mjs +18 -0
- package/registry/frameworks/next/lib/agent/index.ts +95 -0
- package/registry/frameworks/next/lib/agent/middleware.ts +40 -0
- package/registry/frameworks/next/lib/agent/tools.ts +66 -0
- package/registry/frameworks/next/lib/chat/threads-client.ts +57 -0
- package/registry/frameworks/next/lib/server/registry.ts +57 -0
- package/registry/frameworks/next/lib/server/serialize.ts +32 -0
- package/registry/frameworks/next/lib/server/session.ts +212 -0
- package/registry/frameworks/next/lib/server/threads.ts +406 -0
- package/registry/frameworks/next/next.config.ts +7 -0
- package/registry/frameworks/next/package.json +37 -0
- package/registry/frameworks/next/postcss.config.mjs +7 -0
- package/registry/frameworks/next/public/file.svg +1 -0
- package/registry/frameworks/next/public/globe.svg +1 -0
- package/registry/frameworks/next/public/next.svg +1 -0
- package/registry/frameworks/next/public/vercel.svg +1 -0
- package/registry/frameworks/next/public/window.svg +1 -0
- package/registry/frameworks/next/tsconfig.json +34 -0
- package/registry/frameworks/next.ts +17 -0
- package/registry/frameworks/nuxt/.env.example +3 -0
- package/registry/frameworks/nuxt/README.md +133 -0
- package/registry/frameworks/nuxt/app/app.vue +26 -0
- package/registry/frameworks/nuxt/app/assets/css/main.css +707 -0
- package/registry/frameworks/nuxt/app/components/Chat.vue +105 -0
- package/registry/frameworks/nuxt/app/components/ChatApp.vue +89 -0
- package/registry/frameworks/nuxt/app/components/ChatThread.vue +27 -0
- package/registry/frameworks/nuxt/app/components/MessageBubble.vue +60 -0
- package/registry/frameworks/nuxt/app/components/MessageBubbles.vue +213 -0
- package/registry/frameworks/nuxt/app/components/MessageList.vue +51 -0
- package/registry/frameworks/nuxt/app/components/MessageReasoning.vue +53 -0
- package/registry/frameworks/nuxt/app/components/StreamingIndicator.vue +9 -0
- package/registry/frameworks/nuxt/app/components/SubagentDetail.vue +51 -0
- package/registry/frameworks/nuxt/app/components/SubagentList.vue +49 -0
- package/registry/frameworks/nuxt/app/components/ThemeToggle.vue +43 -0
- package/registry/frameworks/nuxt/app/components/ThreadHistory.vue +65 -0
- package/registry/frameworks/nuxt/app/components/ToolCall.vue +81 -0
- package/registry/frameworks/nuxt/app/components/TypingDots.vue +14 -0
- package/registry/frameworks/nuxt/app/composables/useTheme.ts +14 -0
- package/registry/frameworks/nuxt/app/utils/streaming.ts +44 -0
- package/registry/frameworks/nuxt/app/utils/threads.ts +57 -0
- package/registry/frameworks/nuxt/nuxt.config.ts +6 -0
- package/registry/frameworks/nuxt/package.json +28 -0
- package/registry/frameworks/nuxt/public/favicon.ico +0 -0
- package/registry/frameworks/nuxt/public/robots.txt +2 -0
- package/registry/frameworks/nuxt/server/agent/index.ts +89 -0
- package/registry/frameworks/nuxt/server/agent/middleware.ts +38 -0
- package/registry/frameworks/nuxt/server/agent/tools.ts +66 -0
- package/registry/frameworks/nuxt/server/api/threads/[threadId]/commands.post.ts +16 -0
- package/registry/frameworks/nuxt/server/api/threads/[threadId]/history.post.ts +37 -0
- package/registry/frameworks/nuxt/server/api/threads/[threadId]/index.delete.ts +12 -0
- package/registry/frameworks/nuxt/server/api/threads/[threadId]/state.get.ts +22 -0
- package/registry/frameworks/nuxt/server/api/threads/[threadId]/state.post.ts +32 -0
- package/registry/frameworks/nuxt/server/api/threads/[threadId]/stream.post.ts +24 -0
- package/registry/frameworks/nuxt/server/api/threads/index.get.ts +13 -0
- package/registry/frameworks/nuxt/server/utils/runtime.ts +42 -0
- package/registry/frameworks/nuxt/server/utils/serialize.ts +30 -0
- package/registry/frameworks/nuxt/server/utils/session.ts +210 -0
- package/registry/frameworks/nuxt/server/utils/threads.ts +404 -0
- package/registry/frameworks/nuxt/tsconfig.json +18 -0
- package/registry/frameworks/nuxt.ts +17 -0
- package/registry/frameworks/vite/.env.example +20 -0
- package/registry/frameworks/vite/README.md +149 -0
- package/registry/frameworks/vite/agent/index.ts +59 -0
- package/registry/frameworks/vite/agent/middleware.ts +24 -0
- package/registry/frameworks/vite/agent/tools.ts +64 -0
- package/registry/frameworks/vite/index.html +23 -0
- package/registry/frameworks/vite/langgraph.json +16 -0
- package/registry/frameworks/vite/package.json +39 -0
- package/registry/frameworks/vite/public/favicon.ico +0 -0
- package/registry/frameworks/vite/scripts/vite-langgraph-proxy.ts +34 -0
- package/registry/frameworks/vite/src/components/Chat.tsx +124 -0
- package/registry/frameworks/vite/src/components/ChatApp.tsx +122 -0
- package/registry/frameworks/vite/src/components/Conversation.tsx +91 -0
- package/registry/frameworks/vite/src/components/MessageBubbles.tsx +88 -0
- package/registry/frameworks/vite/src/components/MessageReasoning.tsx +71 -0
- package/registry/frameworks/vite/src/components/MessageThread.tsx +135 -0
- package/registry/frameworks/vite/src/components/StreamingIndicator.tsx +36 -0
- package/registry/frameworks/vite/src/components/Subagents.tsx +120 -0
- package/registry/frameworks/vite/src/components/ThemeIcons.tsx +31 -0
- package/registry/frameworks/vite/src/components/ThreadHistory.tsx +73 -0
- package/registry/frameworks/vite/src/components/ToolCall.tsx +89 -0
- package/registry/frameworks/vite/src/lib/agent-type.ts +4 -0
- package/registry/frameworks/vite/src/lib/chat/threads-client.ts +114 -0
- package/registry/frameworks/vite/src/main.tsx +11 -0
- package/registry/frameworks/vite/src/styles/globals.css +714 -0
- package/registry/frameworks/vite/src/vite-env.d.ts +11 -0
- package/registry/frameworks/vite/tsconfig.app.json +24 -0
- package/registry/frameworks/vite/tsconfig.json +7 -0
- package/registry/frameworks/vite/tsconfig.node.json +21 -0
- package/registry/frameworks/vite/vercel.json +3 -0
- package/registry/frameworks/vite/vite.config.ts +24 -0
- package/registry/frameworks/vite.ts +17 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { tool } from "langchain";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Mock tools used to demonstrate message and tool-call streaming.
|
|
6
|
+
*
|
|
7
|
+
* Both tools are intentionally fake so the example runs offline. What matters
|
|
8
|
+
* is that the agent (and its subagents) emit real tool-call deltas on the
|
|
9
|
+
* `messages` channel and tool results as `ToolMessage`s, which the UI renders.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const searchWeb = tool(
|
|
13
|
+
async ({ query }) => {
|
|
14
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
15
|
+
return JSON.stringify({
|
|
16
|
+
results: [
|
|
17
|
+
{
|
|
18
|
+
title: `Result for: ${query}`,
|
|
19
|
+
snippet:
|
|
20
|
+
"LangGraph streaming sends token deltas on the messages channel " +
|
|
21
|
+
"and tool lifecycle events on the tools channel.",
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "search_web",
|
|
28
|
+
description: "Search the web for information about a topic.",
|
|
29
|
+
schema: z.object({ query: z.string().describe("Search query.") }),
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
/** Demo-only arithmetic evaluator restricted to numbers and basic operators. */
|
|
34
|
+
function evaluateExpression(expression: string): number {
|
|
35
|
+
if (!/^[\d+\-*/().\s]+$/.test(expression)) {
|
|
36
|
+
throw new Error("Only basic arithmetic is supported.");
|
|
37
|
+
}
|
|
38
|
+
const compute = new Function(
|
|
39
|
+
`"use strict"; return (${expression});`,
|
|
40
|
+
) as () => unknown;
|
|
41
|
+
const result = compute();
|
|
42
|
+
if (typeof result !== "number" || !Number.isFinite(result)) {
|
|
43
|
+
throw new Error("Expression did not evaluate to a finite number.");
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const calculator = tool(
|
|
49
|
+
async ({ expression }) => {
|
|
50
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
51
|
+
try {
|
|
52
|
+
return String(evaluateExpression(expression));
|
|
53
|
+
} catch (error) {
|
|
54
|
+
return `Error evaluating: ${expression} (${String(error)})`;
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "calculator",
|
|
59
|
+
description: "Evaluate a math expression.",
|
|
60
|
+
schema: z.object({
|
|
61
|
+
expression: z.string().describe("Math expression to evaluate."),
|
|
62
|
+
}),
|
|
63
|
+
},
|
|
64
|
+
);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<meta
|
|
8
|
+
name="description"
|
|
9
|
+
content="Deploying a LangChain deep agent with LangSmith Deployment: streaming chat, subagents, and per-thread history."
|
|
10
|
+
/>
|
|
11
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
12
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
13
|
+
<link
|
|
14
|
+
href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500&family=Inter:wght@400;500;600&display=swap"
|
|
15
|
+
rel="stylesheet"
|
|
16
|
+
/>
|
|
17
|
+
<title>LangChain Deep Agent · LangSmith</title>
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
<div id="root"></div>
|
|
21
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
22
|
+
</body>
|
|
23
|
+
</html>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://langgra.ph/schema.json",
|
|
3
|
+
"node_version": "22",
|
|
4
|
+
"dependencies": ["."],
|
|
5
|
+
"graphs": {
|
|
6
|
+
"agent": "./agent/index.ts:agent"
|
|
7
|
+
},
|
|
8
|
+
"env": ".env",
|
|
9
|
+
"http": {
|
|
10
|
+
"cors": {
|
|
11
|
+
"allow_origins": ["*"],
|
|
12
|
+
"allow_methods": ["GET", "POST", "DELETE", "OPTIONS"],
|
|
13
|
+
"allow_headers": ["Authorization", "Content-Type", "X-Api-Key"]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "js-langsmith",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "concurrently -n agent,web -c blue,green \"pnpm dev:agent\" \"pnpm dev:web\"",
|
|
8
|
+
"dev:agent": "langgraphjs dev --no-browser --host 127.0.0.1 -p 2025",
|
|
9
|
+
"dev:web": "wait-on http-get://127.0.0.1:2025/ok && vite",
|
|
10
|
+
"build": "vite build",
|
|
11
|
+
"preview": "vite preview",
|
|
12
|
+
"deploy": "langgraphjs deploy",
|
|
13
|
+
"typecheck": "tsc --noEmit -p tsconfig.app.json && tsc --noEmit -p tsconfig.node.json"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@langchain/core": "^1.2.1",
|
|
17
|
+
"@langchain/langgraph": "^1.3.7",
|
|
18
|
+
"@langchain/langgraph-sdk": "^1.9.20",
|
|
19
|
+
"@langchain/openai": "^1.4.7",
|
|
20
|
+
"@langchain/react": "^1.0.20",
|
|
21
|
+
"deepagents": "^1.10.2",
|
|
22
|
+
"langchain": "^1.4.4",
|
|
23
|
+
"react": "19.2.4",
|
|
24
|
+
"react-dom": "19.2.4",
|
|
25
|
+
"zod": "^4.4.3"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@langchain/langgraph-cli": "^1.0.0",
|
|
29
|
+
"@types/node": "^20",
|
|
30
|
+
"@types/react": "^19",
|
|
31
|
+
"@types/react-dom": "^19",
|
|
32
|
+
"@vitejs/plugin-react": "^4.4.1",
|
|
33
|
+
"concurrently": "^9.2.0",
|
|
34
|
+
"typescript": "^5",
|
|
35
|
+
"vite": "^6.3.5",
|
|
36
|
+
"wait-on": "^8.0.3"
|
|
37
|
+
},
|
|
38
|
+
"packageManager": "pnpm@10.29.2"
|
|
39
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ProxyOptions } from "vite";
|
|
2
|
+
|
|
3
|
+
const LANGGRAPH_UPSTREAM =
|
|
4
|
+
process.env.LANGGRAPH_PROXY_TARGET ?? "http://127.0.0.1:2025";
|
|
5
|
+
|
|
6
|
+
const UPSTREAM_PROXY: Omit<ProxyOptions, "rewrite"> = {
|
|
7
|
+
target: LANGGRAPH_UPSTREAM,
|
|
8
|
+
changeOrigin: true,
|
|
9
|
+
timeout: 600_000,
|
|
10
|
+
proxyTimeout: 600_000,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Vite dev-server proxy rules for LangGraph API traffic.
|
|
15
|
+
*
|
|
16
|
+
* Clients use `apiUrl = origin + "/api/langgraph"`. The LangGraph SDK may
|
|
17
|
+
* also resolve paths like `/threads/...` from the dev-server root, so mirror
|
|
18
|
+
* allowlisted upstream routes at both locations.
|
|
19
|
+
*/
|
|
20
|
+
export function createLangGraphViteProxy(): Record<string, ProxyOptions> {
|
|
21
|
+
return {
|
|
22
|
+
"/api/langgraph": {
|
|
23
|
+
...UPSTREAM_PROXY,
|
|
24
|
+
rewrite: (path) => path.replace(/^\/api\/langgraph/, ""),
|
|
25
|
+
},
|
|
26
|
+
"/threads": UPSTREAM_PROXY,
|
|
27
|
+
"/runs": UPSTREAM_PROXY,
|
|
28
|
+
"/assistants": UPSTREAM_PROXY,
|
|
29
|
+
"/sandbox": UPSTREAM_PROXY,
|
|
30
|
+
"/download": UPSTREAM_PROXY,
|
|
31
|
+
"/ok": UPSTREAM_PROXY,
|
|
32
|
+
"/info": UPSTREAM_PROXY,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import { HumanMessage } from "@langchain/core/messages";
|
|
6
|
+
import { useStreamContext } from "@langchain/react";
|
|
7
|
+
|
|
8
|
+
import type { Agent } from "@/lib/agent-type";
|
|
9
|
+
import { Conversation } from "./Conversation";
|
|
10
|
+
import { SubagentDetail } from "./Subagents";
|
|
11
|
+
|
|
12
|
+
const EXAMPLE_PROMPT =
|
|
13
|
+
"Research LangGraph streaming, and separately calculate 42 * 17.";
|
|
14
|
+
|
|
15
|
+
export function Chat({
|
|
16
|
+
onRunSettled,
|
|
17
|
+
}: {
|
|
18
|
+
threadId: string;
|
|
19
|
+
/** Called when a run settles, so the sidebar can refresh titles/order. */
|
|
20
|
+
onRunSettled: () => void;
|
|
21
|
+
}) {
|
|
22
|
+
const stream = useStreamContext<Agent>();
|
|
23
|
+
const [content, setContent] = useState(EXAMPLE_PROMPT);
|
|
24
|
+
const [openSubagentId, setOpenSubagentId] = useState<string | null>(null);
|
|
25
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
26
|
+
|
|
27
|
+
// Refresh the sidebar whenever a run finishes (titles derive from the first
|
|
28
|
+
// message; order from the latest checkpoint, both owned by the server).
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!stream.isLoading) onRunSettled();
|
|
31
|
+
}, [stream.isLoading, onRunSettled]);
|
|
32
|
+
|
|
33
|
+
function autoGrow() {
|
|
34
|
+
const node = textareaRef.current;
|
|
35
|
+
if (!node) return;
|
|
36
|
+
node.style.height = "auto";
|
|
37
|
+
node.style.height = `${Math.min(node.scrollHeight, 200)}px`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const subagents = [...stream.subagents.values()];
|
|
41
|
+
const openSubagent = openSubagentId
|
|
42
|
+
? subagents.find((snapshot) => snapshot.id === openSubagentId)
|
|
43
|
+
: undefined;
|
|
44
|
+
|
|
45
|
+
function handleSubmit() {
|
|
46
|
+
const nextContent = content.trim();
|
|
47
|
+
if (nextContent.length === 0 || stream.isLoading) return;
|
|
48
|
+
|
|
49
|
+
setContent("");
|
|
50
|
+
if (textareaRef.current) textareaRef.current.style.height = "auto";
|
|
51
|
+
void stream.submit({
|
|
52
|
+
messages: [new HumanMessage(nextContent)],
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Subagent detail view: breadcrumb + that subagent's chat (no composer).
|
|
57
|
+
if (openSubagent) {
|
|
58
|
+
return (
|
|
59
|
+
<main className="chat-main">
|
|
60
|
+
<nav aria-label="Breadcrumb" className="breadcrumb">
|
|
61
|
+
<button
|
|
62
|
+
className="crumb-link"
|
|
63
|
+
onClick={() => setOpenSubagentId(null)}
|
|
64
|
+
type="button"
|
|
65
|
+
>
|
|
66
|
+
Main chat
|
|
67
|
+
</button>
|
|
68
|
+
<span className="crumb-sep">/</span>
|
|
69
|
+
<span className="crumb-current">{openSubagent.name}</span>
|
|
70
|
+
</nav>
|
|
71
|
+
<div className="conversation">
|
|
72
|
+
<div className="conversation-inner">
|
|
73
|
+
<SubagentDetail snapshot={openSubagent} />
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</main>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Main view: messages + subagent chips, with the composer pinned at the bottom.
|
|
81
|
+
return (
|
|
82
|
+
<main className="chat-main">
|
|
83
|
+
<div className="conversation">
|
|
84
|
+
<div className="conversation-inner">
|
|
85
|
+
<Conversation onOpenSubagent={setOpenSubagentId} />
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<div className="composer-bar">
|
|
90
|
+
<form
|
|
91
|
+
className="composer"
|
|
92
|
+
onSubmit={(event) => {
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
handleSubmit();
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
<textarea
|
|
98
|
+
aria-label="Message"
|
|
99
|
+
onChange={(event) => {
|
|
100
|
+
setContent(event.target.value);
|
|
101
|
+
autoGrow();
|
|
102
|
+
}}
|
|
103
|
+
onKeyDown={(event) => {
|
|
104
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
105
|
+
event.preventDefault();
|
|
106
|
+
handleSubmit();
|
|
107
|
+
}
|
|
108
|
+
}}
|
|
109
|
+
placeholder="Ask for research, a calculation, or both..."
|
|
110
|
+
ref={textareaRef}
|
|
111
|
+
rows={1}
|
|
112
|
+
value={content}
|
|
113
|
+
/>
|
|
114
|
+
<button
|
|
115
|
+
disabled={content.trim() === "" || stream.isLoading}
|
|
116
|
+
type="submit"
|
|
117
|
+
>
|
|
118
|
+
Send
|
|
119
|
+
</button>
|
|
120
|
+
</form>
|
|
121
|
+
</div>
|
|
122
|
+
</main>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import { StreamProvider } from "@langchain/react";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
type ThreadSummary,
|
|
9
|
+
createThread,
|
|
10
|
+
deleteThread,
|
|
11
|
+
fetchThreads,
|
|
12
|
+
getAgentApiUrl,
|
|
13
|
+
getApiKey,
|
|
14
|
+
} from "@/lib/chat/threads-client";
|
|
15
|
+
import { Chat } from "./Chat";
|
|
16
|
+
import { ThreadHistory } from "./ThreadHistory";
|
|
17
|
+
import { MoonIcon, SunIcon } from "./ThemeIcons";
|
|
18
|
+
|
|
19
|
+
/** Graph id from `agent/langgraph.json`. */
|
|
20
|
+
const ASSISTANT_ID = "agent";
|
|
21
|
+
|
|
22
|
+
export function ChatApp() {
|
|
23
|
+
const [mounted, setMounted] = useState(false);
|
|
24
|
+
const [theme, setTheme] = useState<"dark" | "light">("dark");
|
|
25
|
+
const [threads, setThreads] = useState<ThreadSummary[]>([]);
|
|
26
|
+
const [threadId, setThreadId] = useState<string>("");
|
|
27
|
+
const initStarted = useRef(false);
|
|
28
|
+
|
|
29
|
+
const refreshThreads = useCallback(async () => {
|
|
30
|
+
setThreads(await fetchThreads());
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (initStarted.current) return;
|
|
35
|
+
initStarted.current = true;
|
|
36
|
+
void (async () => {
|
|
37
|
+
const list = await fetchThreads();
|
|
38
|
+
if (list.length > 0) {
|
|
39
|
+
setThreads(list);
|
|
40
|
+
setThreadId(list[0].id);
|
|
41
|
+
} else {
|
|
42
|
+
const id = await createThread();
|
|
43
|
+
setThreads(await fetchThreads());
|
|
44
|
+
setThreadId(id);
|
|
45
|
+
}
|
|
46
|
+
setMounted(true);
|
|
47
|
+
})();
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const handleSelect = useCallback(
|
|
51
|
+
(id: string) => {
|
|
52
|
+
if (id !== threadId) setThreadId(id);
|
|
53
|
+
},
|
|
54
|
+
[threadId],
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const handleCreate = useCallback(async () => {
|
|
58
|
+
const id = await createThread();
|
|
59
|
+
await refreshThreads();
|
|
60
|
+
setThreadId(id);
|
|
61
|
+
}, [refreshThreads]);
|
|
62
|
+
|
|
63
|
+
const handleDelete = useCallback(
|
|
64
|
+
async (id: string) => {
|
|
65
|
+
await deleteThread(id);
|
|
66
|
+
const list = await fetchThreads();
|
|
67
|
+
setThreads(list);
|
|
68
|
+
if (id !== threadId) return;
|
|
69
|
+
if (list.length > 0) {
|
|
70
|
+
setThreadId(list[0].id);
|
|
71
|
+
} else {
|
|
72
|
+
const freshId = await createThread();
|
|
73
|
+
setThreads(await fetchThreads());
|
|
74
|
+
setThreadId(freshId);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
[threadId],
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const shellClassName = `app-shell ${theme === "light" ? "light" : ""}`;
|
|
81
|
+
|
|
82
|
+
if (!mounted || !threadId) {
|
|
83
|
+
return (
|
|
84
|
+
<div className={shellClassName}>
|
|
85
|
+
<div className="empty-state center">Preparing chat…</div>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className={shellClassName}>
|
|
92
|
+
<button
|
|
93
|
+
aria-label={
|
|
94
|
+
theme === "dark" ? "Switch to light mode" : "Switch to dark mode"
|
|
95
|
+
}
|
|
96
|
+
className="theme-toggle"
|
|
97
|
+
onClick={() => setTheme((cur) => (cur === "dark" ? "light" : "dark"))}
|
|
98
|
+
type="button"
|
|
99
|
+
>
|
|
100
|
+
{theme === "dark" ? <SunIcon /> : <MoonIcon />}
|
|
101
|
+
</button>
|
|
102
|
+
|
|
103
|
+
<ThreadHistory
|
|
104
|
+
activeThreadId={threadId}
|
|
105
|
+
onCreate={handleCreate}
|
|
106
|
+
onDelete={handleDelete}
|
|
107
|
+
onSelect={handleSelect}
|
|
108
|
+
threads={threads}
|
|
109
|
+
/>
|
|
110
|
+
|
|
111
|
+
<StreamProvider
|
|
112
|
+
key={threadId}
|
|
113
|
+
assistantId={ASSISTANT_ID}
|
|
114
|
+
apiUrl={getAgentApiUrl()}
|
|
115
|
+
apiKey={getApiKey()}
|
|
116
|
+
threadId={threadId}
|
|
117
|
+
>
|
|
118
|
+
<Chat onRunSettled={refreshThreads} threadId={threadId} />
|
|
119
|
+
</StreamProvider>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
|
|
5
|
+
import type { SubagentDiscoverySnapshot } from "@langchain/langgraph-sdk/stream";
|
|
6
|
+
import { useStreamContext } from "@langchain/react";
|
|
7
|
+
|
|
8
|
+
import type { Agent } from "@/lib/agent-type";
|
|
9
|
+
import { MessageThread } from "./MessageThread";
|
|
10
|
+
import {
|
|
11
|
+
shouldShowTypingIndicator,
|
|
12
|
+
StreamingIndicator,
|
|
13
|
+
} from "./StreamingIndicator";
|
|
14
|
+
import { SubagentList, type SubagentCard } from "./Subagents";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The root conversation, rendered in message order.
|
|
18
|
+
*
|
|
19
|
+
* Subagent `task` delegations are rendered as cards (not raw tool rows) at the
|
|
20
|
+
* position they occur, and all other tool calls are folded into collapsible
|
|
21
|
+
* tool-call chips by {@link MessageThread}.
|
|
22
|
+
*/
|
|
23
|
+
export function Conversation({
|
|
24
|
+
onOpenSubagent,
|
|
25
|
+
}: {
|
|
26
|
+
onOpenSubagent: (id: string) => void;
|
|
27
|
+
}) {
|
|
28
|
+
const stream = useStreamContext<Agent>();
|
|
29
|
+
|
|
30
|
+
const messages = useMemo(
|
|
31
|
+
() => stream.messages.filter((message) => message != null),
|
|
32
|
+
[stream.messages],
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const subagentsById = useMemo(() => {
|
|
36
|
+
const map = new Map<string, SubagentDiscoverySnapshot>();
|
|
37
|
+
for (const snapshot of stream.subagents.values()) {
|
|
38
|
+
map.set(snapshot.id, snapshot);
|
|
39
|
+
}
|
|
40
|
+
return map;
|
|
41
|
+
}, [stream.subagents]);
|
|
42
|
+
|
|
43
|
+
const showTypingIndicator = shouldShowTypingIndicator(
|
|
44
|
+
messages,
|
|
45
|
+
stream.isLoading,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<>
|
|
50
|
+
{messages.length === 0 && !stream.error ? (
|
|
51
|
+
<div className="empty-state">
|
|
52
|
+
Ask a question below. The coordinator will delegate to its subagents
|
|
53
|
+
and stream tokens, tool calls, and results.
|
|
54
|
+
</div>
|
|
55
|
+
) : null}
|
|
56
|
+
|
|
57
|
+
<MessageThread
|
|
58
|
+
isLoading={stream.isLoading}
|
|
59
|
+
messages={messages}
|
|
60
|
+
taskRenderer={(tasks) => {
|
|
61
|
+
const cards: SubagentCard[] = tasks.map((call, index) => {
|
|
62
|
+
const snapshot = call.id ? subagentsById.get(call.id) : undefined;
|
|
63
|
+
const args = (call.args ?? {}) as Record<string, unknown>;
|
|
64
|
+
return {
|
|
65
|
+
id: call.id ?? `task-${index}`,
|
|
66
|
+
name: snapshot?.name ?? String(args.subagent_type ?? "subagent"),
|
|
67
|
+
task:
|
|
68
|
+
snapshot?.taskInput ??
|
|
69
|
+
(typeof args.description === "string"
|
|
70
|
+
? args.description
|
|
71
|
+
: undefined),
|
|
72
|
+
status: snapshot?.status ?? "running",
|
|
73
|
+
openable: snapshot != null,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
return <SubagentList cards={cards} onOpen={onOpenSubagent} />;
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
|
|
80
|
+
{showTypingIndicator ? <StreamingIndicator /> : null}
|
|
81
|
+
|
|
82
|
+
{messages.length === 0 && !stream.isLoading && stream.error ? (
|
|
83
|
+
<div className="error">
|
|
84
|
+
Could not reach the agent API. Make sure the Deno server is running
|
|
85
|
+
and
|
|
86
|
+
<code>OPENAI_API_KEY</code> is set, then try again.
|
|
87
|
+
</div>
|
|
88
|
+
) : null}
|
|
89
|
+
</>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { AIMessage, type BaseMessage } from "@langchain/core/messages";
|
|
4
|
+
|
|
5
|
+
type ToolCallLike = {
|
|
6
|
+
name: string;
|
|
7
|
+
args?: Record<string, unknown>;
|
|
8
|
+
id?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function messageLabel(message: { type: string; name?: string }) {
|
|
12
|
+
if (message.type === "human") return "You";
|
|
13
|
+
if (message.type === "tool") return `Tool · ${message.name ?? "result"}`;
|
|
14
|
+
if (message.type === "ai") return "Assistant";
|
|
15
|
+
return message.type;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatToolArgs(args: Record<string, unknown>) {
|
|
19
|
+
const entries = Object.entries(args);
|
|
20
|
+
if (entries.length === 0) return "";
|
|
21
|
+
if (entries.length === 1) return String(entries[0]?.[1] ?? "");
|
|
22
|
+
return JSON.stringify(args);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Extract reasoning-summary text from a message.
|
|
27
|
+
*
|
|
28
|
+
* Reasoning models surface their summaries as `{ type: "reasoning" }` standard
|
|
29
|
+
* content blocks (see `@langchain/openai`'s Responses API converter). Only AI
|
|
30
|
+
* messages carry reasoning; everything else returns an empty string.
|
|
31
|
+
*/
|
|
32
|
+
export function getReasoningText(message: BaseMessage): string {
|
|
33
|
+
if (!AIMessage.isInstance(message)) return "";
|
|
34
|
+
try {
|
|
35
|
+
return message.contentBlocks
|
|
36
|
+
.filter(
|
|
37
|
+
(block): block is { type: "reasoning"; reasoning: string } =>
|
|
38
|
+
(block as { type?: string })?.type === "reasoning",
|
|
39
|
+
)
|
|
40
|
+
.map((block) => block.reasoning)
|
|
41
|
+
.join("")
|
|
42
|
+
.trim();
|
|
43
|
+
} catch {
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Renders a single message as a chat bubble with its tool-call rows.
|
|
50
|
+
*
|
|
51
|
+
* `toolCalls` can be passed to override which tool calls are shown (e.g. to
|
|
52
|
+
* hide the `task` calls that are rendered as subagent cards instead).
|
|
53
|
+
*/
|
|
54
|
+
export function MessageBubble({
|
|
55
|
+
message,
|
|
56
|
+
toolCalls,
|
|
57
|
+
}: {
|
|
58
|
+
message: BaseMessage;
|
|
59
|
+
toolCalls?: ToolCallLike[];
|
|
60
|
+
}) {
|
|
61
|
+
const calls =
|
|
62
|
+
toolCalls ??
|
|
63
|
+
(AIMessage.isInstance(message) ? (message.tool_calls ?? []) : []);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
className={`message ${message.type === "human" ? "user" : ""} ${
|
|
68
|
+
message.type === "tool" ? "tool" : ""
|
|
69
|
+
}`}
|
|
70
|
+
>
|
|
71
|
+
<span>{messageLabel(message)}</span>
|
|
72
|
+
{calls.length > 0 ? (
|
|
73
|
+
<ul className="tool-call-list">
|
|
74
|
+
{calls.map((toolCall, toolIndex) => {
|
|
75
|
+
const args = formatToolArgs(toolCall.args ?? {});
|
|
76
|
+
return (
|
|
77
|
+
<li key={toolCall.id ?? toolIndex}>
|
|
78
|
+
<strong>{toolCall.name}</strong>
|
|
79
|
+
{args ? `(${args})` : ""}
|
|
80
|
+
</li>
|
|
81
|
+
);
|
|
82
|
+
})}
|
|
83
|
+
</ul>
|
|
84
|
+
) : null}
|
|
85
|
+
{message.text ? <p>{message.text}</p> : null}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import { TypingDots } from "./StreamingIndicator";
|
|
6
|
+
|
|
7
|
+
function BrainIcon() {
|
|
8
|
+
return (
|
|
9
|
+
<svg
|
|
10
|
+
aria-hidden
|
|
11
|
+
fill="none"
|
|
12
|
+
stroke="currentColor"
|
|
13
|
+
strokeLinecap="round"
|
|
14
|
+
strokeLinejoin="round"
|
|
15
|
+
strokeWidth="1.6"
|
|
16
|
+
viewBox="0 0 24 24"
|
|
17
|
+
>
|
|
18
|
+
<path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z" />
|
|
19
|
+
<path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z" />
|
|
20
|
+
<path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4" />
|
|
21
|
+
<path d="M6 18a4 4 0 0 1-1.967-.516" />
|
|
22
|
+
<path d="M19.967 17.484A4 4 0 0 1 18 18" />
|
|
23
|
+
</svg>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Minimalistic reasoning block: a `Thinking` toggle with a brain icon and a
|
|
29
|
+
* caret, rendered inline in the conversation (not inside a message bubble).
|
|
30
|
+
*
|
|
31
|
+
* While reasoning tokens stream (`active`), the block auto-expands so you can
|
|
32
|
+
* watch the model think; once the turn finishes it auto-collapses. The caret
|
|
33
|
+
* stays clickable so a finished block can be re-opened.
|
|
34
|
+
*/
|
|
35
|
+
export function MessageReasoning({
|
|
36
|
+
reasoning,
|
|
37
|
+
active,
|
|
38
|
+
}: {
|
|
39
|
+
reasoning: string;
|
|
40
|
+
active: boolean;
|
|
41
|
+
}) {
|
|
42
|
+
const [open, setOpen] = useState(active);
|
|
43
|
+
|
|
44
|
+
// Follow the streaming state: expand on start, collapse on finish. The effect
|
|
45
|
+
// only runs when `active` flips, so a manual toggle in between is preserved.
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
48
|
+
setOpen(active);
|
|
49
|
+
}, [active]);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className={`reasoning ${open ? "open" : ""}`}>
|
|
53
|
+
<button
|
|
54
|
+
aria-expanded={open}
|
|
55
|
+
className="reasoning-toggle"
|
|
56
|
+
onClick={() => setOpen((value) => !value)}
|
|
57
|
+
type="button"
|
|
58
|
+
>
|
|
59
|
+
<span aria-hidden className="reasoning-caret">
|
|
60
|
+
▸
|
|
61
|
+
</span>
|
|
62
|
+
<span aria-hidden className="reasoning-icon">
|
|
63
|
+
<BrainIcon />
|
|
64
|
+
</span>
|
|
65
|
+
<span className="reasoning-label">Thinking</span>
|
|
66
|
+
{active ? <TypingDots className="inline-dots reasoning-dots" /> : null}
|
|
67
|
+
</button>
|
|
68
|
+
{open ? <p className="reasoning-text">{reasoning}</p> : null}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|