diff --git a/apps/dojo/e2e/featurePages/SharedStatePage.ts b/apps/dojo/e2e/featurePages/SharedStatePage.ts index 6ac0554b13..794265ef1c 100644 --- a/apps/dojo/e2e/featurePages/SharedStatePage.ts +++ b/apps/dojo/e2e/featurePages/SharedStatePage.ts @@ -38,6 +38,13 @@ export class SharedStatePage { await expect(this.agentGreeting).toBeVisible(); } + async awaitAgentReady() { + await expect(this.page.getByTestId("recipe-card")).toHaveAttribute( + "data-agent-ready", + "true", + ); + } + async sendMessage(message: string) { await sendChatMessage(this.page, message); await awaitLLMResponseDone(this.page); diff --git a/apps/dojo/e2e/tests/langgraphPythonTests/agenticChatPage.event-trace.ts b/apps/dojo/e2e/tests/langgraphPythonTests/agenticChatPage.event-trace.ts index bdcf5777a9..007311a1bd 100644 --- a/apps/dojo/e2e/tests/langgraphPythonTests/agenticChatPage.event-trace.ts +++ b/apps/dojo/e2e/tests/langgraphPythonTests/agenticChatPage.event-trace.ts @@ -1,7 +1,7 @@ import { defineEventTrace } from "../../event-trace-test"; // Generated by the event trace updater. -// Reason: Re-record on the langgraph 1.2.x examples environment (required by the deepagents integration): the runtime no longer injects empty auth/request metadata and dependency version strings moved, which shifts placeholder numbering throughout. +// Reason: Ignore late LangGraph messages tuples once events mode is active so obsolete snapshots are not emitted. // Before accepting changes, verify whether the implementation regressed. const shared1 = { output: null, @@ -4514,54 +4514,6 @@ const shared46 = { ], } as const; const shared47 = { - event: "messages", - data: [ - { - content: "", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - }, - type: "AIMessageChunk", - name: null, - id: "id-11", - tool_calls: [], - invalid_tool_calls: [], - usage_metadata: null, - tool_call_chunks: [], - chunk_position: null, - }, - { - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_api_version: "", - langgraph_plan: "enterprise", - langgraph_host: "self-hosted", - langgraph_api_url: "", - run_id: "id-5", - thread_id: "id-1", - langgraph_step: 2, - langgraph_node: "model", - langgraph_triggers: ["branch:to:model"], - langgraph_path: ["__pregel_pull", "model"], - langgraph_checkpoint_ns: "model:id-9", - checkpoint_ns: "model:id-9", - ls_provider: "openai", - ls_model_name: "gpt-4.1-mini", - ls_model_type: "chat", - ls_temperature: null, - lc_versions: { - "langchain-core": "1.5.3", - langchain: "1.3.14", - }, - }, - ], -} as const; -const shared48 = { messages: [ { content: "Hi change the background color to blue", @@ -4608,7 +4560,7 @@ const shared48 = { ], }, } as const; -const shared49 = { +const shared48 = { messages: [ { content: @@ -4655,7 +4607,7 @@ const shared49 = { ], }, } as const; -const shared50 = { +const shared49 = { messages: [ { content: "Hi, I am duaa", @@ -4702,7 +4654,7 @@ const shared50 = { ], }, } as const; -const shared51 = { +const shared50 = { messages: [ { content: @@ -4749,7 +4701,7 @@ const shared51 = { ], }, } as const; -const shared52 = { +const shared51 = { messages: [ { content: "Hey there", @@ -4796,7 +4748,7 @@ const shared52 = { ], }, } as const; -const shared53 = { +const shared52 = { messages: [ { content: @@ -4843,7 +4795,7 @@ const shared53 = { ], }, } as const; -const shared54 = { +const shared53 = { messages: [ { content: "Hi change the background color to blue", @@ -4890,7 +4842,7 @@ const shared54 = { original_ai_message_id: "id-11", }, } as const; -const shared55 = { +const shared54 = { event: "events", data: { event: "on_chain_stream", @@ -4922,7 +4874,7 @@ const shared55 = { parent_ids: ["id-5"], }, } as const; -const shared56 = { +const shared55 = { event: "events", data: { event: "on_chain_stream", @@ -4954,7 +4906,7 @@ const shared56 = { parent_ids: ["id-5"], }, } as const; -const shared57 = { +const shared56 = { messages: [ { content: "Hi change the background color to blue", @@ -5001,7 +4953,7 @@ const shared57 = { original_ai_message_id: null, }, } as const; -const shared58 = { +const shared57 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5020,7 +4972,7 @@ const shared58 = { langgraph_path: ["__pregel_pull", "CopilotKitMiddleware.before_agent"], langgraph_checkpoint_ns: "CopilotKitMiddleware.before_agent:id-33", } as const; -const shared59 = { +const shared58 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5039,7 +4991,7 @@ const shared59 = { langgraph_path: ["__pregel_pull", "CopilotKitMiddleware.before_agent"], langgraph_checkpoint_ns: "CopilotKitMiddleware.before_agent:id-34", } as const; -const shared60 = { +const shared59 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5058,7 +5010,7 @@ const shared60 = { langgraph_path: ["__pregel_pull", "CopilotKitMiddleware.before_agent"], langgraph_checkpoint_ns: "CopilotKitMiddleware.before_agent:id-46", } as const; -const shared61 = { +const shared60 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5077,7 +5029,7 @@ const shared61 = { langgraph_path: ["__pregel_pull", "CopilotKitMiddleware.before_agent"], langgraph_checkpoint_ns: "CopilotKitMiddleware.before_agent:id-48", } as const; -const shared62 = { +const shared61 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5096,7 +5048,7 @@ const shared62 = { langgraph_path: ["__pregel_pull", "CopilotKitMiddleware.before_agent"], langgraph_checkpoint_ns: "CopilotKitMiddleware.before_agent:id-59", } as const; -const shared63 = { +const shared62 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5115,7 +5067,7 @@ const shared63 = { langgraph_path: ["__pregel_pull", "CopilotKitMiddleware.before_agent"], langgraph_checkpoint_ns: "CopilotKitMiddleware.before_agent:id-20", } as const; -const shared64 = { +const shared63 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5134,7 +5086,7 @@ const shared64 = { langgraph_path: ["__pregel_pull", "CopilotKitMiddleware.before_agent"], langgraph_checkpoint_ns: "CopilotKitMiddleware.before_agent:id-21", } as const; -const shared65 = { +const shared64 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5153,7 +5105,7 @@ const shared65 = { langgraph_path: ["__pregel_pull", "CopilotKitMiddleware.after_agent"], langgraph_checkpoint_ns: "CopilotKitMiddleware.after_agent:id-28", } as const; -const shared66 = { +const shared65 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5172,7 +5124,7 @@ const shared66 = { langgraph_path: ["__pregel_pull", "CopilotKitMiddleware.after_agent"], langgraph_checkpoint_ns: "CopilotKitMiddleware.after_agent:id-29", } as const; -const shared67 = { +const shared66 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5191,7 +5143,7 @@ const shared67 = { langgraph_path: ["__pregel_pull", "CopilotKitMiddleware.after_model"], langgraph_checkpoint_ns: "CopilotKitMiddleware.after_model:id-39", } as const; -const shared68 = { +const shared67 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5210,7 +5162,7 @@ const shared68 = { langgraph_path: ["__pregel_pull", "CopilotKitMiddleware.after_agent"], langgraph_checkpoint_ns: "CopilotKitMiddleware.after_agent:id-41", } as const; -const shared69 = { +const shared68 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5229,7 +5181,7 @@ const shared69 = { langgraph_path: ["__pregel_pull", "CopilotKitMiddleware.after_model"], langgraph_checkpoint_ns: "CopilotKitMiddleware.after_model:id-41", } as const; -const shared70 = { +const shared69 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5248,7 +5200,7 @@ const shared70 = { langgraph_path: ["__pregel_pull", "CopilotKitMiddleware.after_agent"], langgraph_checkpoint_ns: "CopilotKitMiddleware.after_agent:id-43", } as const; -const shared71 = { +const shared70 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5267,7 +5219,7 @@ const shared71 = { langgraph_path: ["__pregel_pull", "CopilotKitMiddleware.after_model"], langgraph_checkpoint_ns: "CopilotKitMiddleware.after_model:id-52", } as const; -const shared72 = { +const shared71 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5286,7 +5238,7 @@ const shared72 = { langgraph_path: ["__pregel_pull", "CopilotKitMiddleware.after_agent"], langgraph_checkpoint_ns: "CopilotKitMiddleware.after_agent:id-54", } as const; -const shared73 = { +const shared72 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5305,7 +5257,7 @@ const shared73 = { langgraph_path: ["__pregel_pull", "CopilotKitMiddleware.after_model"], langgraph_checkpoint_ns: "CopilotKitMiddleware.after_model:id-54", } as const; -const shared74 = { +const shared73 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5324,7 +5276,7 @@ const shared74 = { langgraph_path: ["__pregel_pull", "CopilotKitMiddleware.after_agent"], langgraph_checkpoint_ns: "CopilotKitMiddleware.after_agent:id-56", } as const; -const shared75 = { +const shared74 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5343,7 +5295,7 @@ const shared75 = { langgraph_path: ["__pregel_pull", "CopilotKitMiddleware.after_model"], langgraph_checkpoint_ns: "CopilotKitMiddleware.after_model:id-65", } as const; -const shared76 = { +const shared75 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5362,7 +5314,7 @@ const shared76 = { langgraph_path: ["__pregel_pull", "CopilotKitMiddleware.after_agent"], langgraph_checkpoint_ns: "CopilotKitMiddleware.after_agent:id-67", } as const; -const shared77 = { +const shared76 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5381,7 +5333,7 @@ const shared77 = { langgraph_path: ["__pregel_pull", "CopilotKitMiddleware.after_model"], langgraph_checkpoint_ns: "CopilotKitMiddleware.after_model:id-26", } as const; -const shared78 = { +const shared77 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5400,7 +5352,7 @@ const shared78 = { langgraph_path: ["__pregel_pull", "CopilotKitMiddleware.after_model"], langgraph_checkpoint_ns: "CopilotKitMiddleware.after_model:id-27", } as const; -const shared79 = { +const shared78 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5419,7 +5371,7 @@ const shared79 = { langgraph_path: ["__pregel_pull", "CopilotKitMiddleware.after_model"], langgraph_checkpoint_ns: "CopilotKitMiddleware.after_model:id-14", } as const; -const shared80 = { +const shared79 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5438,7 +5390,7 @@ const shared80 = { langgraph_path: ["__pregel_pull", "CopilotKitMiddleware.after_agent"], langgraph_checkpoint_ns: "CopilotKitMiddleware.after_agent:id-16", } as const; -const shared81 = { +const shared80 = { messages: [ [ { @@ -5471,7 +5423,7 @@ const shared81 = { ], ], } as const; -const shared82 = { +const shared81 = { messages: [ [ { @@ -5504,7 +5456,7 @@ const shared82 = { ], ], } as const; -const shared83 = { +const shared82 = { messages: [ [ { @@ -5537,7 +5489,7 @@ const shared83 = { ], ], } as const; -const shared84 = { +const shared83 = { messages: [ { content: "Hi change the background color to blue", @@ -5575,7 +5527,7 @@ const shared84 = { ], }, } as const; -const shared85 = { +const shared84 = { messages: [ { content: "Hi, I am duaa", @@ -5613,7 +5565,7 @@ const shared85 = { ], }, } as const; -const shared86 = { +const shared85 = { messages: [ { content: "Hey there", @@ -5651,7 +5603,7 @@ const shared86 = { ], }, } as const; -const shared87 = { +const shared86 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5670,7 +5622,7 @@ const shared87 = { langgraph_path: ["__pregel_pull", "model"], langgraph_checkpoint_ns: "model:id-35", } as const; -const shared88 = { +const shared87 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5689,7 +5641,7 @@ const shared88 = { langgraph_path: ["__pregel_pull", "model"], langgraph_checkpoint_ns: "model:id-36", } as const; -const shared89 = { +const shared88 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5708,7 +5660,7 @@ const shared89 = { langgraph_path: ["__pregel_pull", "model"], langgraph_checkpoint_ns: "model:id-48", } as const; -const shared90 = { +const shared89 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5727,7 +5679,7 @@ const shared90 = { langgraph_path: ["__pregel_pull", "model"], langgraph_checkpoint_ns: "model:id-50", } as const; -const shared91 = { +const shared90 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5746,7 +5698,7 @@ const shared91 = { langgraph_path: ["__pregel_pull", "model"], langgraph_checkpoint_ns: "model:id-61", } as const; -const shared92 = { +const shared91 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5765,7 +5717,7 @@ const shared92 = { langgraph_path: ["__pregel_pull", "model"], langgraph_checkpoint_ns: "model:id-22", } as const; -const shared93 = { +const shared92 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5784,7 +5736,7 @@ const shared93 = { langgraph_path: ["__pregel_pull", "model"], langgraph_checkpoint_ns: "model:id-23", } as const; -const shared94 = { +const shared93 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5803,7 +5755,7 @@ const shared94 = { langgraph_path: ["__pregel_pull", "model"], langgraph_checkpoint_ns: "model:id-9", } as const; -const shared95 = [ +const shared94 = [ { graph: null, update: { @@ -5830,7 +5782,7 @@ const shared95 = [ goto: [], }, ] as const; -const shared96 = [ +const shared95 = [ { graph: null, update: { @@ -5857,7 +5809,7 @@ const shared96 = [ goto: [], }, ] as const; -const shared97 = [ +const shared96 = [ { graph: null, update: { @@ -5892,7 +5844,7 @@ const shared97 = [ goto: [], }, ] as const; -const shared98 = [ +const shared97 = [ { graph: null, update: { @@ -5927,7 +5879,7 @@ const shared98 = [ goto: [], }, ] as const; -const shared99 = { +const shared98 = { ls_integration: "langchain_create_agent", created_by: "system", graph_id: "agentic_chat", @@ -5941,7 +5893,7 @@ const shared99 = { run_id: "id-5", thread_id: "id-1", } as const; -const shared100 = [ +const shared99 = [ { graph: null, update: { @@ -5968,7 +5920,7 @@ const shared100 = [ goto: [], }, ] as const; -const shared101 = [ +const shared100 = [ { name: "change_background", description: @@ -5985,7 +5937,7 @@ const shared101 = [ }, }, ] as const; -const shared102 = [ +const shared101 = [ { graph: null, update: { @@ -6011,7 +5963,7 @@ const shared102 = [ goto: [], }, ] as const; -const shared103 = [ +const shared102 = [ { graph: null, update: { @@ -6037,7 +5989,7 @@ const shared103 = [ goto: [], }, ] as const; -const shared104 = [ +const shared103 = [ { graph: null, update: { @@ -6063,7 +6015,7 @@ const shared104 = [ goto: [], }, ] as const; -const shared105 = [ +const shared104 = [ { graph: null, update: { @@ -6089,7 +6041,7 @@ const shared105 = [ goto: [], }, ] as const; -const shared106 = [ +const shared105 = [ { graph: null, update: { @@ -6115,125 +6067,20 @@ const shared106 = [ goto: [], }, ] as const; -const shared107 = { +const shared106 = { id: "id-7", role: "system", content: 'App Context:\n{\n "copilotkit_forwarded_headers": {\n "x-forwarded-for": "",\n "x-forwarded-host": "",\n "x-forwarded-port": "",\n "x-forwarded-proto": ""\n }\n}', } as const; -const shared108 = { - content: "", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - }, - type: "AIMessageChunk", - name: null, - id: "id-24", - tool_calls: [], - invalid_tool_calls: [], - usage_metadata: null, - tool_call_chunks: [], - chunk_position: null, -} as const; -const shared109 = { - content: "", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - }, - type: "AIMessageChunk", - name: null, - id: "id-25", - tool_calls: [], - invalid_tool_calls: [], - usage_metadata: null, - tool_call_chunks: [], - chunk_position: null, -} as const; -const shared110 = { - content: "", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - }, - type: "AIMessageChunk", - name: null, - id: "id-37", - tool_calls: [], - invalid_tool_calls: [], - usage_metadata: null, - tool_call_chunks: [], - chunk_position: null, -} as const; -const shared111 = { - content: "", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - }, - type: "AIMessageChunk", - name: null, - id: "id-38", - tool_calls: [], - invalid_tool_calls: [], - usage_metadata: null, - tool_call_chunks: [], - chunk_position: null, -} as const; -const shared112 = { - content: "", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - }, - type: "AIMessageChunk", - name: null, - id: "id-50", - tool_calls: [], - invalid_tool_calls: [], - usage_metadata: null, - tool_call_chunks: [], - chunk_position: null, -} as const; -const shared113 = { - content: "", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - }, - type: "AIMessageChunk", - name: null, - id: "id-52", - tool_calls: [], - invalid_tool_calls: [], - usage_metadata: null, - tool_call_chunks: [], - chunk_position: null, -} as const; -const shared114 = { - content: "", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - }, - type: "AIMessageChunk", - name: null, - id: "id-63", - tool_calls: [], - invalid_tool_calls: [], - usage_metadata: null, - tool_call_chunks: [], - chunk_position: null, -} as const; -const shared115 = { +const shared107 = { id: "id-50", role: "assistant", content: "The Moon is Earth's only natural satellite, orbiting at an average distance of about 384,400 km. It takes approximately 27.3 days to complete one orbit.", toolCalls: [], } as const; -const shared116 = { +const shared108 = { id: "id-11", role: "assistant", content: "", @@ -6248,7 +6095,7 @@ const shared116 = { }, ], } as const; -const shared117 = { +const shared109 = { id: "id-38", role: "assistant", content: "", @@ -6263,39 +6110,39 @@ const shared117 = { }, ], } as const; -const shared118 = { +const shared110 = { id: "id-37", role: "assistant", content: "Kaavish is a wonderful musical group known for their unique blend of Eastern and Western sounds!", toolCalls: [], } as const; -const shared119 = { +const shared111 = { id: "id-24", role: "assistant", content: "That's great! Mango is a wonderful tropical fruit known for its sweet, juicy flavor.", toolCalls: [], } as const; -const shared120 = { +const shared112 = { id: "id-18", role: "tool", content: '{"status":"success","message":"Background changed to blue"}', toolCallId: "id-12", } as const; -const shared121 = { +const shared113 = { id: "id-11", role: "assistant", content: "Hello! How can I assist you today?", toolCalls: [], } as const; -const shared122 = { +const shared114 = { id: "id-25", role: "assistant", content: "Done! I've completed that for you.", toolCalls: [], } as const; -const shared123 = { +const shared115 = { id: "id-56", role: "user", content: "Can you remind me what my favorite fruit is?", @@ -6318,7 +6165,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { content: "Hi change the background color to blue", }, ], - tools: shared101, + tools: shared100, context: [ { description: "Name of the user", @@ -6334,13 +6181,13 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared84, + snapshot: shared83, rawEvent: { event: "events", data: { event: "on_chain_start", data: { - input: shared84, + input: shared83, }, name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:1"], @@ -6373,14 +6220,14 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared49, + snapshot: shared48, rawEvent: { event: "events", data: { event: "on_chain_end", data: { - output: shared49, - input: shared84, + output: shared48, + input: shared83, }, run_id: "id-4", name: "CopilotKitMiddleware.before_agent", @@ -6413,7 +6260,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared49, + snapshot: shared48, rawEvent: shared46, }, { @@ -6426,31 +6273,31 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared48, + snapshot: shared47, rawEvent: { event: "events", data: { event: "on_chain_start", data: { - input: shared48, + input: shared47, }, name: "model", tags: ["graph:step:2"], run_id: "id-8", - metadata: shared94, + metadata: shared93, parent_ids: ["id-5"], }, }, }, { type: "STATE_SNAPSHOT", - snapshot: shared48, + snapshot: shared47, rawEvent: { event: "events", data: { event: "on_chat_model_start", data: { - input: shared81, + input: shared80, }, name: "ChatOpenAI", tags: ["seq:step:1"], @@ -6489,17 +6336,12 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared48, + snapshot: shared47, rawEvent: shared36, }, { type: "STATE_SNAPSHOT", - snapshot: shared48, - rawEvent: shared47, - }, - { - type: "STATE_SNAPSHOT", - snapshot: shared48, + snapshot: shared47, rawEvent: { event: "events", data: shared29, @@ -6689,7 +6531,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { invalid_tool_calls: [], usage_metadata: null, }, - input: shared81, + input: shared80, }, run_id: "id-10", name: "ChatOpenAI", @@ -6727,7 +6569,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared48, + snapshot: shared47, rawEvent: { event: "events", data: { @@ -6735,9 +6577,9 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-8", name: "model", tags: ["graph:step:2"], - metadata: shared94, + metadata: shared93, data: { - chunk: shared97, + chunk: shared96, }, parent_ids: ["id-5"], }, @@ -6745,19 +6587,19 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared48, + snapshot: shared47, rawEvent: { event: "events", data: { event: "on_chain_end", data: { - output: shared97, - input: shared48, + output: shared96, + input: shared47, }, run_id: "id-8", name: "model", tags: ["graph:step:2"], - metadata: shared94, + metadata: shared93, parent_ids: ["id-5"], }, }, @@ -6783,7 +6625,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_model", tags: ["graph:step:3"], run_id: "id-13", - metadata: shared79, + metadata: shared78, parent_ids: ["id-5"], }, }, @@ -6798,9 +6640,9 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-13", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:3"], - metadata: shared79, + metadata: shared78, data: { - chunk: shared54, + chunk: shared53, }, parent_ids: ["id-5"], }, @@ -6808,19 +6650,19 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared54, + snapshot: shared53, rawEvent: { event: "events", data: { event: "on_chain_end", data: { - output: shared54, + output: shared53, input: shared30, }, run_id: "id-13", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:3"], - metadata: shared79, + metadata: shared78, parent_ids: ["id-5"], }, }, @@ -6835,25 +6677,25 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared54, + snapshot: shared53, rawEvent: { event: "events", data: { event: "on_chain_start", data: { - input: shared54, + input: shared53, }, name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:4"], run_id: "id-15", - metadata: shared80, + metadata: shared79, parent_ids: ["id-5"], }, }, }, { type: "STATE_SNAPSHOT", - snapshot: shared54, + snapshot: shared53, rawEvent: { event: "events", data: { @@ -6861,9 +6703,9 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-15", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:4"], - metadata: shared80, + metadata: shared79, data: { - chunk: shared57, + chunk: shared56, }, parent_ids: ["id-5"], }, @@ -6871,37 +6713,37 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared57, + snapshot: shared56, rawEvent: { event: "events", data: { event: "on_chain_end", data: { - output: shared57, - input: shared54, + output: shared56, + input: shared53, }, run_id: "id-15", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:4"], - metadata: shared80, + metadata: shared79, parent_ids: ["id-5"], }, }, }, { type: "STATE_SNAPSHOT", - snapshot: shared57, + snapshot: shared56, rawEvent: { event: "events", data: { event: "on_chain_end", data: { - output: shared57, + output: shared56, }, run_id: "id-5", name: "agentic_chat", tags: [], - metadata: shared99, + metadata: shared98, parent_ids: [], }, }, @@ -6912,7 +6754,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared57, + snapshot: shared56, }, { type: "MESSAGES_SNAPSHOT", @@ -6922,8 +6764,8 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { role: "user", content: "Hi change the background color to blue", }, - shared107, - shared116, + shared106, + shared108, ], }, { @@ -6938,9 +6780,9 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { input: { threadId: "id-1", runId: "id-17", - state: shared57, + state: shared56, messages: [ - shared116, + shared108, { id: "id-18", content: @@ -6948,9 +6790,9 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { role: "tool", toolCallId: "id-12", }, - shared107, + shared106, ], - tools: shared101, + tools: shared100, context: [ { description: "Name of the user", @@ -7060,7 +6902,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:7"], run_id: "id-19", - metadata: shared64, + metadata: shared63, parent_ids: ["id-20"], }, }, @@ -7334,7 +7176,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-19", name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:7"], - metadata: shared64, + metadata: shared63, parent_ids: ["id-20"], }, }, @@ -7443,7 +7285,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "model", tags: ["graph:step:8"], run_id: "id-22", - metadata: shared93, + metadata: shared92, parent_ids: ["id-20"], }, }, @@ -7671,136 +7513,26 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { data: { event: "on_chat_model_stream", data: { - chunk: shared109, - }, - run_id: "id-24", - name: "ChatOpenAI", - tags: ["seq:step:1"], - metadata: { - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_api_version: "", - langgraph_plan: "enterprise", - langgraph_host: "self-hosted", - langgraph_api_url: "", - run_id: "id-20", - thread_id: "id-1", - langgraph_step: 8, - langgraph_node: "model", - langgraph_triggers: ["branch:to:model"], - langgraph_path: ["__pregel_pull", "model"], - langgraph_checkpoint_ns: "model:id-23", - checkpoint_ns: "model:id-23", - ls_provider: "openai", - ls_model_name: "gpt-4.1-mini", - ls_model_type: "chat", - ls_temperature: null, - lc_versions: { - "langchain-core": "1.5.3", - langchain: "1.3.14", - }, - }, - parent_ids: ["id-20", "id-22"], - }, - }, - }, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - content: "Hi change the background color to blue", - additional_kwargs: { - type: "human", - }, - response_metadata: {}, - type: "human", - name: null, - id: "id-3", - }, - { - content: - 'App Context:\n{\n "copilotkit_forwarded_headers": {\n "x-forwarded-for": "",\n "x-forwarded-host": "",\n "x-forwarded-port": "",\n "x-forwarded-proto": ""\n }\n}', - additional_kwargs: {}, - response_metadata: {}, - type: "system", - name: null, - id: "id-7", - }, - { - content: "", - additional_kwargs: { + chunk: { + content: "", + additional_kwargs: {}, + response_metadata: { + model_provider: "openai", + }, + type: "AIMessageChunk", + name: null, + id: "id-25", + tool_calls: [], invalid_tool_calls: [], usage_metadata: null, + tool_call_chunks: [], + chunk_position: null, }, - response_metadata: {}, - type: "ai", - name: null, - id: "id-11", - tool_calls: [ - { - name: "change_background", - args: { - background: "blue", - }, - id: "id-12", - type: "tool_call", - }, - ], - invalid_tool_calls: [], - usage_metadata: null, }, - { - content: - '{"status":"success","message":"Background changed to blue"}', - additional_kwargs: { - type: "tool", - }, - response_metadata: {}, - type: "tool", - name: null, - id: "id-18", - tool_call_id: "id-12", - artifact: null, - status: "success", - }, - ], - copilotkit: { - intercepted_tool_calls: null, - original_ai_message_id: null, - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], - }, - }, - rawEvent: { - event: "messages", - data: [ - shared109, - { + run_id: "id-24", + name: "ChatOpenAI", + tags: ["seq:step:1"], + metadata: { ls_integration: "langchain_chat_model", created_by: "system", graph_id: "agentic_chat", @@ -7828,7 +7560,8 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { langchain: "1.3.14", }, }, - ], + parent_ids: ["id-20", "id-22"], + }, }, }, { @@ -8145,9 +7878,9 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-22", name: "model", tags: ["graph:step:8"], - metadata: shared93, + metadata: shared92, data: { - chunk: shared103, + chunk: shared102, }, parent_ids: ["id-20"], }, @@ -8246,7 +7979,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { data: { event: "on_chain_end", data: { - output: shared103, + output: shared102, input: { messages: [ { @@ -8337,7 +8070,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-22", name: "model", tags: ["graph:step:8"], - metadata: shared93, + metadata: shared92, parent_ids: ["id-20"], }, }, @@ -8461,7 +8194,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_model", tags: ["graph:step:9"], run_id: "id-26", - metadata: shared78, + metadata: shared77, parent_ids: ["id-20"], }, }, @@ -8576,7 +8309,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-26", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:9"], - metadata: shared78, + metadata: shared77, data: { chunk: null, }, @@ -8695,7 +8428,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-26", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:9"], - metadata: shared78, + metadata: shared77, parent_ids: ["id-20"], }, }, @@ -8819,7 +8552,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:10"], run_id: "id-28", - metadata: shared66, + metadata: shared65, parent_ids: ["id-20"], }, }, @@ -8934,7 +8667,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-28", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:10"], - metadata: shared66, + metadata: shared65, data: { chunk: null, }, @@ -9053,7 +8786,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-28", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:10"], - metadata: shared66, + metadata: shared65, parent_ids: ["id-20"], }, }, @@ -9405,10 +9138,10 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { role: "user", content: "Hi change the background color to blue", }, - shared107, - shared116, - shared120, - shared122, + shared106, + shared108, + shared112, + shared114, ], }, { @@ -9531,7 +9264,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { content: "Hi change the background color to pink", }, ], - tools: shared101, + tools: shared100, context: [ { description: "Name of the user", @@ -9669,7 +9402,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:13"], run_id: "id-32", - metadata: shared59, + metadata: shared58, parent_ids: ["id-33"], }, }, @@ -10027,7 +9760,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-32", name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:13"], - metadata: shared59, + metadata: shared58, parent_ids: ["id-33"], }, }, @@ -10164,7 +9897,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "model", tags: ["graph:step:14"], run_id: "id-35", - metadata: shared88, + metadata: shared87, parent_ids: ["id-33"], }, }, @@ -10448,7 +10181,21 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { data: { event: "on_chat_model_stream", data: { - chunk: shared111, + chunk: { + content: "", + additional_kwargs: {}, + response_metadata: { + model_provider: "openai", + }, + type: "AIMessageChunk", + name: null, + id: "id-38", + tool_calls: [], + invalid_tool_calls: [], + usage_metadata: null, + tool_call_chunks: [], + chunk_position: null, + }, }, run_id: "id-37", name: "ChatOpenAI", @@ -10602,191 +10349,40 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, }, rawEvent: { - event: "messages", - data: [ - shared111, - { - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_api_version: "", - langgraph_plan: "enterprise", - langgraph_host: "self-hosted", - langgraph_api_url: "", - run_id: "id-33", - thread_id: "id-1", - langgraph_step: 14, - langgraph_node: "model", - langgraph_triggers: ["branch:to:model"], - langgraph_path: ["__pregel_pull", "model"], - langgraph_checkpoint_ns: "model:id-36", - checkpoint_ns: "model:id-36", - ls_provider: "openai", - ls_model_name: "gpt-4.1-mini", - ls_model_type: "chat", - ls_temperature: null, - lc_versions: { - "langchain-core": "1.5.3", - langchain: "1.3.14", - }, - }, - ], + event: "events", + data: shared28, }, }, { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - content: "Hi change the background color to blue", - additional_kwargs: { - type: "human", - }, - response_metadata: {}, - type: "human", - name: null, - id: "id-3", - }, - { - content: - 'App Context:\n{\n "copilotkit_forwarded_headers": {\n "x-forwarded-for": "",\n "x-forwarded-host": "",\n "x-forwarded-port": "",\n "x-forwarded-proto": ""\n }\n}', - additional_kwargs: {}, - response_metadata: {}, - type: "system", - name: null, - id: "id-7", - }, - { + type: "TOOL_CALL_START", + toolCallId: "id-39", + toolCallName: "change_background", + parentMessageId: "id-38", + rawEvent: shared28, + }, + { + type: "TOOL_CALL_ARGS", + toolCallId: "id-39", + delta: '{"background":"pink"', + rawEvent: { + event: "on_chat_model_stream", + data: { + chunk: { content: "", - additional_kwargs: { - invalid_tool_calls: [], - usage_metadata: null, + additional_kwargs: {}, + response_metadata: { + model_provider: "openai", }, - response_metadata: {}, - type: "ai", + type: "AIMessageChunk", name: null, - id: "id-11", + id: "id-38", tool_calls: [ { - name: "change_background", + name: "", args: { - background: "blue", + background: "pink", }, - id: "id-12", - type: "tool_call", - }, - ], - invalid_tool_calls: [], - usage_metadata: null, - }, - { - content: - '{"status":"success","message":"Background changed to blue"}', - additional_kwargs: { - type: "tool", - }, - response_metadata: {}, - type: "tool", - name: null, - id: "id-18", - tool_call_id: "id-12", - artifact: null, - status: "success", - }, - { - content: "Done! I've completed that for you.", - additional_kwargs: { - invalid_tool_calls: [], - usage_metadata: null, - }, - response_metadata: { - finish_reason: "stop", - model_name: "gpt-4.1-mini", - model_provider: "openai", - }, - type: "ai", - name: null, - id: "id-25", - tool_calls: [], - invalid_tool_calls: [], - usage_metadata: null, - }, - { - content: "Hi change the background color to pink", - additional_kwargs: { - type: "human", - }, - response_metadata: {}, - type: "human", - name: null, - id: "id-31", - }, - ], - copilotkit: { - intercepted_tool_calls: null, - original_ai_message_id: null, - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], - }, - }, - rawEvent: { - event: "events", - data: shared28, - }, - }, - { - type: "TOOL_CALL_START", - toolCallId: "id-39", - toolCallName: "change_background", - parentMessageId: "id-38", - rawEvent: shared28, - }, - { - type: "TOOL_CALL_ARGS", - toolCallId: "id-39", - delta: '{"background":"pink"', - rawEvent: { - event: "on_chat_model_stream", - data: { - chunk: { - content: "", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - }, - type: "AIMessageChunk", - name: null, - id: "id-38", - tool_calls: [ - { - name: "", - args: { - background: "pink", - }, - id: null, + id: null, type: "tool_call", }, ], @@ -11100,9 +10696,9 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-35", name: "model", tags: ["graph:step:14"], - metadata: shared88, + metadata: shared87, data: { - chunk: shared98, + chunk: shared97, }, parent_ids: ["id-33"], }, @@ -11229,7 +10825,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { data: { event: "on_chain_end", data: { - output: shared98, + output: shared97, input: { messages: [ { @@ -11348,7 +10944,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-35", name: "model", tags: ["graph:step:14"], - metadata: shared88, + metadata: shared87, parent_ids: ["id-33"], }, }, @@ -11374,7 +10970,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_model", tags: ["graph:step:15"], run_id: "id-40", - metadata: shared69, + metadata: shared68, parent_ids: ["id-33"], }, }, @@ -11389,7 +10985,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-40", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:15"], - metadata: shared69, + metadata: shared68, data: { chunk: shared18, }, @@ -11411,7 +11007,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-40", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:15"], - metadata: shared69, + metadata: shared68, parent_ids: ["id-33"], }, }, @@ -11437,7 +11033,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:16"], run_id: "id-42", - metadata: shared70, + metadata: shared69, parent_ids: ["id-33"], }, }, @@ -11452,7 +11048,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-42", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:16"], - metadata: shared70, + metadata: shared69, data: { chunk: shared20, }, @@ -11474,7 +11070,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-42", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:16"], - metadata: shared70, + metadata: shared69, parent_ids: ["id-33"], }, }, @@ -11526,16 +11122,16 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { role: "user", content: "Hi change the background color to blue", }, - shared107, - shared116, - shared120, - shared122, + shared106, + shared108, + shared112, + shared114, { id: "id-31", role: "user", content: "Hi change the background color to pink", }, - shared117, + shared109, ], }, { @@ -11552,7 +11148,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { runId: "id-44", state: shared20, messages: [ - shared117, + shared109, { id: "id-45", content: @@ -11561,7 +11157,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { toolCallId: "id-39", }, ], - tools: shared101, + tools: shared100, context: [ { description: "Name of the user", @@ -11736,7 +11332,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:19"], run_id: "id-46", - metadata: shared61, + metadata: shared60, parent_ids: ["id-47"], }, }, @@ -12205,7 +11801,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-46", name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:19"], - metadata: shared61, + metadata: shared60, parent_ids: ["id-47"], }, }, @@ -12379,7 +11975,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "model", tags: ["graph:step:20"], run_id: "id-49", - metadata: shared90, + metadata: shared89, parent_ids: ["id-47"], }, }, @@ -12737,7 +12333,21 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { data: { event: "on_chat_model_stream", data: { - chunk: shared113, + chunk: { + content: "", + additional_kwargs: {}, + response_metadata: { + model_provider: "openai", + }, + type: "AIMessageChunk", + name: null, + id: "id-52", + tool_calls: [], + invalid_tool_calls: [], + usage_metadata: null, + tool_call_chunks: [], + chunk_position: null, + }, }, run_id: "id-51", name: "ChatOpenAI", @@ -12928,231 +12538,43 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, }, rawEvent: { - event: "messages", - data: [ - shared113, - { - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_api_version: "", - langgraph_plan: "enterprise", - langgraph_host: "self-hosted", - langgraph_api_url: "", - run_id: "id-47", - thread_id: "id-1", - langgraph_step: 20, - langgraph_node: "model", - langgraph_triggers: ["branch:to:model"], - langgraph_path: ["__pregel_pull", "model"], - langgraph_checkpoint_ns: "model:id-50", - checkpoint_ns: "model:id-50", - ls_provider: "openai", - ls_model_name: "gpt-4.1-mini", - ls_model_type: "chat", - ls_temperature: null, - lc_versions: { - "langchain-core": "1.5.3", - langchain: "1.3.14", - }, - }, - ], + event: "events", + data: shared37, }, }, { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - content: "Hi change the background color to blue", - additional_kwargs: { - type: "human", - }, - response_metadata: {}, - type: "human", - name: null, - id: "id-3", - }, - { - content: - 'App Context:\n{\n "copilotkit_forwarded_headers": {\n "x-forwarded-for": "",\n "x-forwarded-host": "",\n "x-forwarded-port": "",\n "x-forwarded-proto": ""\n }\n}', + type: "TEXT_MESSAGE_START", + role: "assistant", + messageId: "id-52", + rawEvent: shared37, + }, + { + type: "TEXT_MESSAGE_CONTENT", + messageId: "id-52", + delta: "Done! I've completed", + rawEvent: shared37, + }, + { + type: "TEXT_MESSAGE_CONTENT", + messageId: "id-52", + delta: " that for you.", + rawEvent: { + event: "on_chat_model_stream", + data: { + chunk: { + content: " that for you.", additional_kwargs: {}, - response_metadata: {}, - type: "system", - name: null, - id: "id-7", - }, - { - content: "", - additional_kwargs: { - invalid_tool_calls: [], - usage_metadata: null, + response_metadata: { + model_provider: "openai", }, - response_metadata: {}, - type: "ai", + type: "AIMessageChunk", name: null, - id: "id-11", - tool_calls: [ - { - name: "change_background", - args: { - background: "blue", - }, - id: "id-12", - type: "tool_call", - }, - ], + id: "id-52", + tool_calls: [], invalid_tool_calls: [], usage_metadata: null, - }, - { - content: - '{"status":"success","message":"Background changed to blue"}', - additional_kwargs: { - type: "tool", - }, - response_metadata: {}, - type: "tool", - name: null, - id: "id-18", - tool_call_id: "id-12", - artifact: null, - status: "success", - }, - { - content: "Done! I've completed that for you.", - additional_kwargs: { - invalid_tool_calls: [], - usage_metadata: null, - }, - response_metadata: { - finish_reason: "stop", - model_name: "gpt-4.1-mini", - model_provider: "openai", - }, - type: "ai", - name: null, - id: "id-25", - tool_calls: [], - invalid_tool_calls: [], - usage_metadata: null, - }, - { - content: "Hi change the background color to pink", - additional_kwargs: { - type: "human", - }, - response_metadata: {}, - type: "human", - name: null, - id: "id-31", - }, - { - content: "", - additional_kwargs: { - invalid_tool_calls: [], - usage_metadata: null, - }, - response_metadata: {}, - type: "ai", - name: null, - id: "id-38", - tool_calls: [ - { - name: "change_background", - args: { - background: "pink", - }, - id: "id-39", - type: "tool_call", - }, - ], - invalid_tool_calls: [], - usage_metadata: null, - }, - { - content: - '{"status":"success","message":"Background changed to pink"}', - additional_kwargs: { - type: "tool", - }, - response_metadata: {}, - type: "tool", - name: null, - id: "id-45", - tool_call_id: "id-39", - artifact: null, - status: "success", - }, - ], - copilotkit: { - intercepted_tool_calls: null, - original_ai_message_id: null, - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], - }, - }, - rawEvent: { - event: "events", - data: shared37, - }, - }, - { - type: "TEXT_MESSAGE_START", - role: "assistant", - messageId: "id-52", - rawEvent: shared37, - }, - { - type: "TEXT_MESSAGE_CONTENT", - messageId: "id-52", - delta: "Done! I've completed", - rawEvent: shared37, - }, - { - type: "TEXT_MESSAGE_CONTENT", - messageId: "id-52", - delta: " that for you.", - rawEvent: { - event: "on_chat_model_stream", - data: { - chunk: { - content: " that for you.", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - }, - type: "AIMessageChunk", - name: null, - id: "id-52", - tool_calls: [], - invalid_tool_calls: [], - usage_metadata: null, - tool_call_chunks: [], - chunk_position: null, + tool_call_chunks: [], + chunk_position: null, }, }, run_id: "id-51", @@ -13406,9 +12828,9 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-49", name: "model", tags: ["graph:step:20"], - metadata: shared90, + metadata: shared89, data: { - chunk: shared104, + chunk: shared103, }, parent_ids: ["id-47"], }, @@ -13572,7 +12994,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { data: { event: "on_chain_end", data: { - output: shared104, + output: shared103, input: { messages: [ { @@ -13728,7 +13150,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-49", name: "model", tags: ["graph:step:20"], - metadata: shared90, + metadata: shared89, parent_ids: ["id-47"], }, }, @@ -13917,7 +13339,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_model", tags: ["graph:step:21"], run_id: "id-53", - metadata: shared73, + metadata: shared72, parent_ids: ["id-47"], }, }, @@ -14097,7 +13519,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-53", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:21"], - metadata: shared73, + metadata: shared72, data: { chunk: null, }, @@ -14281,7 +13703,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-53", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:21"], - metadata: shared73, + metadata: shared72, parent_ids: ["id-47"], }, }, @@ -14470,7 +13892,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:22"], run_id: "id-55", - metadata: shared74, + metadata: shared73, parent_ids: ["id-47"], }, }, @@ -14650,7 +14072,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-55", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:22"], - metadata: shared74, + metadata: shared73, data: { chunk: null, }, @@ -14834,7 +14256,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-55", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:22"], - metadata: shared74, + metadata: shared73, parent_ids: ["id-47"], }, }, @@ -15381,16 +14803,16 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { role: "user", content: "Hi change the background color to blue", }, - shared107, - shared116, - shared120, - shared122, + shared106, + shared108, + shared112, + shared114, { id: "id-31", role: "user", content: "Hi change the background color to pink", }, - shared117, + shared109, { id: "id-45", role: "tool", @@ -15428,7 +14850,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { content: "Hey there", }, ], - tools: shared101, + tools: shared100, context: [ { description: "Name of the user", @@ -15444,13 +14866,13 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared86, + snapshot: shared85, rawEvent: { event: "events", data: { event: "on_chain_start", data: { - input: shared86, + input: shared85, }, name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:1"], @@ -15483,14 +14905,14 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared53, + snapshot: shared52, rawEvent: { event: "events", data: { event: "on_chain_end", data: { - output: shared53, - input: shared86, + output: shared52, + input: shared85, }, run_id: "id-4", name: "CopilotKitMiddleware.before_agent", @@ -15523,7 +14945,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared53, + snapshot: shared52, rawEvent: shared46, }, { @@ -15536,31 +14958,31 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared52, + snapshot: shared51, rawEvent: { event: "events", data: { event: "on_chain_start", data: { - input: shared52, + input: shared51, }, name: "model", tags: ["graph:step:2"], run_id: "id-8", - metadata: shared94, + metadata: shared93, parent_ids: ["id-5"], }, }, }, { type: "STATE_SNAPSHOT", - snapshot: shared52, + snapshot: shared51, rawEvent: { event: "events", data: { event: "on_chat_model_start", data: { - input: shared83, + input: shared82, }, name: "ChatOpenAI", tags: ["seq:step:1"], @@ -15599,17 +15021,12 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared52, + snapshot: shared51, rawEvent: shared36, }, { type: "STATE_SNAPSHOT", - snapshot: shared52, - rawEvent: shared47, - }, - { - type: "STATE_SNAPSHOT", - snapshot: shared52, + snapshot: shared51, rawEvent: { event: "events", data: shared44, @@ -15705,7 +15122,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { invalid_tool_calls: [], usage_metadata: null, }, - input: shared83, + input: shared82, }, run_id: "id-10", name: "ChatOpenAI", @@ -15743,7 +15160,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared52, + snapshot: shared51, rawEvent: { event: "events", data: { @@ -15751,9 +15168,9 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-8", name: "model", tags: ["graph:step:2"], - metadata: shared94, + metadata: shared93, data: { - chunk: shared105, + chunk: shared104, }, parent_ids: ["id-5"], }, @@ -15761,19 +15178,19 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared52, + snapshot: shared51, rawEvent: { event: "events", data: { event: "on_chain_end", data: { - output: shared105, - input: shared52, + output: shared104, + input: shared51, }, run_id: "id-8", name: "model", tags: ["graph:step:2"], - metadata: shared94, + metadata: shared93, parent_ids: ["id-5"], }, }, @@ -15948,7 +15365,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { ], }, }, - rawEvent: shared55, + rawEvent: shared54, }, { type: "STATE_SNAPSHOT", @@ -16218,7 +15635,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { ], }, }, - rawEvent: shared56, + rawEvent: shared55, }, { type: "STATE_SNAPSHOT", @@ -16453,7 +15870,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-5", name: "agentic_chat", tags: [], - metadata: shared99, + metadata: shared98, parent_ids: [], }, }, @@ -16535,8 +15952,8 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { role: "user", content: "Hey there", }, - shared107, - shared121, + shared106, + shared113, ], }, { @@ -16614,14 +16031,14 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, }, messages: [ - shared107, + shared106, { id: "id-17", role: "user", content: "My favorite fruit is Mango", }, ], - tools: shared101, + tools: shared100, context: [ { description: "Name of the user", @@ -16720,7 +16137,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:7"], run_id: "id-18", - metadata: shared63, + metadata: shared62, parent_ids: ["id-19"], }, }, @@ -16961,7 +16378,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-18", name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:7"], - metadata: shared63, + metadata: shared62, parent_ids: ["id-19"], }, }, @@ -17059,7 +16476,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "model", tags: ["graph:step:8"], run_id: "id-21", - metadata: shared92, + metadata: shared91, parent_ids: ["id-19"], }, }, @@ -17265,7 +16682,21 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { data: { event: "on_chat_model_stream", data: { - chunk: shared108, + chunk: { + content: "", + additional_kwargs: {}, + response_metadata: { + model_provider: "openai", + }, + type: "AIMessageChunk", + name: null, + id: "id-24", + tool_calls: [], + invalid_tool_calls: [], + usage_metadata: null, + tool_call_chunks: [], + chunk_position: null, + }, }, run_id: "id-23", name: "ChatOpenAI", @@ -17380,120 +16811,8 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, }, rawEvent: { - event: "messages", - data: [ - shared108, - { - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_api_version: "", - langgraph_plan: "enterprise", - langgraph_host: "self-hosted", - langgraph_api_url: "", - run_id: "id-19", - thread_id: "id-1", - langgraph_step: 8, - langgraph_node: "model", - langgraph_triggers: ["branch:to:model"], - langgraph_path: ["__pregel_pull", "model"], - langgraph_checkpoint_ns: "model:id-22", - checkpoint_ns: "model:id-22", - ls_provider: "openai", - ls_model_name: "gpt-4.1-mini", - ls_model_type: "chat", - ls_temperature: null, - lc_versions: { - "langchain-core": "1.5.3", - langchain: "1.3.14", - }, - }, - ], - }, - }, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - content: "Hey there", - additional_kwargs: { - type: "human", - }, - response_metadata: {}, - type: "human", - name: null, - id: "id-3", - }, - { - content: - 'App Context:\n{\n "copilotkit_forwarded_headers": {\n "x-forwarded-for": "",\n "x-forwarded-host": "",\n "x-forwarded-port": "",\n "x-forwarded-proto": ""\n }\n}', - additional_kwargs: {}, - response_metadata: {}, - type: "system", - name: null, - id: "id-7", - }, - { - content: "Hello! How can I assist you today?", - additional_kwargs: { - invalid_tool_calls: [], - usage_metadata: null, - }, - response_metadata: { - finish_reason: "stop", - model_name: "gpt-4.1-mini", - model_provider: "openai", - }, - type: "ai", - name: null, - id: "id-11", - tool_calls: [], - invalid_tool_calls: [], - usage_metadata: null, - }, - { - content: "My favorite fruit is Mango", - additional_kwargs: { - type: "human", - }, - response_metadata: {}, - type: "human", - name: null, - id: "id-17", - }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], - }, - }, - rawEvent: { - event: "events", - data: shared42, + event: "events", + data: shared42, }, }, { @@ -17878,9 +17197,9 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-21", name: "model", tags: ["graph:step:8"], - metadata: shared92, + metadata: shared91, data: { - chunk: shared100, + chunk: shared99, }, parent_ids: ["id-19"], }, @@ -17968,7 +17287,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { data: { event: "on_chain_end", data: { - output: shared100, + output: shared99, input: { messages: [ { @@ -18048,7 +17367,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-21", name: "model", tags: ["graph:step:8"], - metadata: shared92, + metadata: shared91, parent_ids: ["id-19"], }, }, @@ -18162,7 +17481,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_model", tags: ["graph:step:9"], run_id: "id-25", - metadata: shared77, + metadata: shared76, parent_ids: ["id-19"], }, }, @@ -18267,7 +17586,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-25", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:9"], - metadata: shared77, + metadata: shared76, data: { chunk: null, }, @@ -18376,7 +17695,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-25", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:9"], - metadata: shared77, + metadata: shared76, parent_ids: ["id-19"], }, }, @@ -18490,7 +17809,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:10"], run_id: "id-27", - metadata: shared65, + metadata: shared64, parent_ids: ["id-19"], }, }, @@ -18595,7 +17914,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-27", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:10"], - metadata: shared65, + metadata: shared64, data: { chunk: null, }, @@ -18704,7 +18023,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-27", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:10"], - metadata: shared65, + metadata: shared64, parent_ids: ["id-19"], }, }, @@ -19026,14 +18345,14 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { role: "user", content: "Hey there", }, - shared107, - shared121, + shared106, + shared113, { id: "id-17", role: "user", content: "My favorite fruit is Mango", }, - shared119, + shared111, ], }, { @@ -19146,7 +18465,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { content: "and I love listening to Kaavish", }, ], - tools: shared101, + tools: shared100, context: [ { description: "Name of the user", @@ -19274,7 +18593,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:13"], run_id: "id-31", - metadata: shared58, + metadata: shared57, parent_ids: ["id-32"], }, }, @@ -19602,7 +18921,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-31", name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:13"], - metadata: shared58, + metadata: shared57, parent_ids: ["id-32"], }, }, @@ -19729,7 +19048,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "model", tags: ["graph:step:14"], run_id: "id-34", - metadata: shared87, + metadata: shared86, parent_ids: ["id-32"], }, }, @@ -19993,7 +19312,21 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { data: { event: "on_chat_model_stream", data: { - chunk: shared110, + chunk: { + content: "", + additional_kwargs: {}, + response_metadata: { + model_provider: "openai", + }, + type: "AIMessageChunk", + name: null, + id: "id-37", + tool_calls: [], + invalid_tool_calls: [], + usage_metadata: null, + tool_call_chunks: [], + chunk_position: null, + }, }, run_id: "id-36", name: "ChatOpenAI", @@ -20136,147 +19469,6 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { ], }, }, - rawEvent: { - event: "messages", - data: [ - shared110, - { - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_api_version: "", - langgraph_plan: "enterprise", - langgraph_host: "self-hosted", - langgraph_api_url: "", - run_id: "id-32", - thread_id: "id-1", - langgraph_step: 14, - langgraph_node: "model", - langgraph_triggers: ["branch:to:model"], - langgraph_path: ["__pregel_pull", "model"], - langgraph_checkpoint_ns: "model:id-35", - checkpoint_ns: "model:id-35", - ls_provider: "openai", - ls_model_name: "gpt-4.1-mini", - ls_model_type: "chat", - ls_temperature: null, - lc_versions: { - "langchain-core": "1.5.3", - langchain: "1.3.14", - }, - }, - ], - }, - }, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - content: "Hey there", - additional_kwargs: { - type: "human", - }, - response_metadata: {}, - type: "human", - name: null, - id: "id-3", - }, - { - content: - 'App Context:\n{\n "copilotkit_forwarded_headers": {\n "x-forwarded-for": "",\n "x-forwarded-host": "",\n "x-forwarded-port": "",\n "x-forwarded-proto": ""\n }\n}', - additional_kwargs: {}, - response_metadata: {}, - type: "system", - name: null, - id: "id-7", - }, - { - content: "Hello! How can I assist you today?", - additional_kwargs: { - invalid_tool_calls: [], - usage_metadata: null, - }, - response_metadata: { - finish_reason: "stop", - model_name: "gpt-4.1-mini", - model_provider: "openai", - }, - type: "ai", - name: null, - id: "id-11", - tool_calls: [], - invalid_tool_calls: [], - usage_metadata: null, - }, - { - content: "My favorite fruit is Mango", - additional_kwargs: { - type: "human", - }, - response_metadata: {}, - type: "human", - name: null, - id: "id-17", - }, - { - content: - "That's great! Mango is a wonderful tropical fruit known for its sweet, juicy flavor.", - additional_kwargs: { - invalid_tool_calls: [], - usage_metadata: null, - }, - response_metadata: { - finish_reason: "stop", - model_name: "gpt-4.1-mini", - model_provider: "openai", - }, - type: "ai", - name: null, - id: "id-24", - tool_calls: [], - invalid_tool_calls: [], - usage_metadata: null, - }, - { - content: "and I love listening to Kaavish", - additional_kwargs: { - type: "human", - }, - response_metadata: {}, - type: "human", - name: null, - id: "id-30", - }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], - }, - }, rawEvent: { event: "events", data: shared38, @@ -20693,9 +19885,9 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-34", name: "model", tags: ["graph:step:14"], - metadata: shared87, + metadata: shared86, data: { - chunk: shared96, + chunk: shared95, }, parent_ids: ["id-32"], }, @@ -20812,7 +20004,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { data: { event: "on_chain_end", data: { - output: shared96, + output: shared95, input: { messages: [ { @@ -20921,7 +20113,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-34", name: "model", tags: ["graph:step:14"], - metadata: shared87, + metadata: shared86, parent_ids: ["id-32"], }, }, @@ -21064,7 +20256,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_model", tags: ["graph:step:15"], run_id: "id-38", - metadata: shared67, + metadata: shared66, parent_ids: ["id-32"], }, }, @@ -21198,7 +20390,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-38", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:15"], - metadata: shared67, + metadata: shared66, data: { chunk: null, }, @@ -21336,7 +20528,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-38", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:15"], - metadata: shared67, + metadata: shared66, parent_ids: ["id-32"], }, }, @@ -21479,7 +20671,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:16"], run_id: "id-40", - metadata: shared68, + metadata: shared67, parent_ids: ["id-32"], }, }, @@ -21613,7 +20805,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-40", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:16"], - metadata: shared68, + metadata: shared67, data: { chunk: null, }, @@ -21751,7 +20943,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-40", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:16"], - metadata: shared68, + metadata: shared67, parent_ids: ["id-32"], }, }, @@ -22160,20 +21352,20 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { role: "user", content: "Hey there", }, - shared107, - shared121, + shared106, + shared113, { id: "id-17", role: "user", content: "My favorite fruit is Mango", }, - shared119, + shared111, { id: "id-30", role: "user", content: "and I love listening to Kaavish", }, - shared118, + shared110, ], }, { @@ -22315,7 +21507,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { content: "tell me an interesting fact about Moon", }, ], - tools: shared101, + tools: shared100, context: [ { description: "Name of the user", @@ -22472,7 +21664,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:19"], run_id: "id-44", - metadata: shared60, + metadata: shared59, parent_ids: ["id-45"], }, }, @@ -22887,7 +22079,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-44", name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:19"], - metadata: shared60, + metadata: shared59, parent_ids: ["id-45"], }, }, @@ -23043,7 +22235,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "model", tags: ["graph:step:20"], run_id: "id-47", - metadata: shared89, + metadata: shared88, parent_ids: ["id-45"], }, }, @@ -23365,7 +22557,21 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { data: { event: "on_chat_model_stream", data: { - chunk: shared112, + chunk: { + content: "", + additional_kwargs: {}, + response_metadata: { + model_provider: "openai", + }, + type: "AIMessageChunk", + name: null, + id: "id-50", + tool_calls: [], + invalid_tool_calls: [], + usage_metadata: null, + tool_call_chunks: [], + chunk_position: null, + }, }, run_id: "id-49", name: "ChatOpenAI", @@ -23537,176 +22743,6 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { ], }, }, - rawEvent: { - event: "messages", - data: [ - shared112, - { - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_api_version: "", - langgraph_plan: "enterprise", - langgraph_host: "self-hosted", - langgraph_api_url: "", - run_id: "id-45", - thread_id: "id-1", - langgraph_step: 20, - langgraph_node: "model", - langgraph_triggers: ["branch:to:model"], - langgraph_path: ["__pregel_pull", "model"], - langgraph_checkpoint_ns: "model:id-48", - checkpoint_ns: "model:id-48", - ls_provider: "openai", - ls_model_name: "gpt-4.1-mini", - ls_model_type: "chat", - ls_temperature: null, - lc_versions: { - "langchain-core": "1.5.3", - langchain: "1.3.14", - }, - }, - ], - }, - }, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - content: "Hey there", - additional_kwargs: { - type: "human", - }, - response_metadata: {}, - type: "human", - name: null, - id: "id-3", - }, - { - content: - 'App Context:\n{\n "copilotkit_forwarded_headers": {\n "x-forwarded-for": "",\n "x-forwarded-host": "",\n "x-forwarded-port": "",\n "x-forwarded-proto": ""\n }\n}', - additional_kwargs: {}, - response_metadata: {}, - type: "system", - name: null, - id: "id-7", - }, - { - content: "Hello! How can I assist you today?", - additional_kwargs: { - invalid_tool_calls: [], - usage_metadata: null, - }, - response_metadata: { - finish_reason: "stop", - model_name: "gpt-4.1-mini", - model_provider: "openai", - }, - type: "ai", - name: null, - id: "id-11", - tool_calls: [], - invalid_tool_calls: [], - usage_metadata: null, - }, - { - content: "My favorite fruit is Mango", - additional_kwargs: { - type: "human", - }, - response_metadata: {}, - type: "human", - name: null, - id: "id-17", - }, - { - content: - "That's great! Mango is a wonderful tropical fruit known for its sweet, juicy flavor.", - additional_kwargs: { - invalid_tool_calls: [], - usage_metadata: null, - }, - response_metadata: { - finish_reason: "stop", - model_name: "gpt-4.1-mini", - model_provider: "openai", - }, - type: "ai", - name: null, - id: "id-24", - tool_calls: [], - invalid_tool_calls: [], - usage_metadata: null, - }, - { - content: "and I love listening to Kaavish", - additional_kwargs: { - type: "human", - }, - response_metadata: {}, - type: "human", - name: null, - id: "id-30", - }, - { - content: - "Kaavish is a wonderful musical group known for their unique blend of Eastern and Western sounds!", - additional_kwargs: { - invalid_tool_calls: [], - usage_metadata: null, - }, - response_metadata: { - finish_reason: "stop", - model_name: "gpt-4.1-mini", - model_provider: "openai", - }, - type: "ai", - name: null, - id: "id-37", - tool_calls: [], - invalid_tool_calls: [], - usage_metadata: null, - }, - { - content: "tell me an interesting fact about Moon", - additional_kwargs: { - type: "human", - }, - response_metadata: {}, - type: "human", - name: null, - id: "id-43", - }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], - }, - }, rawEvent: { event: "events", data: shared39, @@ -24323,9 +23359,9 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-47", name: "model", tags: ["graph:step:20"], - metadata: shared89, + metadata: shared88, data: { - chunk: shared95, + chunk: shared94, }, parent_ids: ["id-45"], }, @@ -24471,7 +23507,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { data: { event: "on_chain_end", data: { - output: shared95, + output: shared94, input: { messages: [ { @@ -24609,7 +23645,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-47", name: "model", tags: ["graph:step:20"], - metadata: shared89, + metadata: shared88, parent_ids: ["id-45"], }, }, @@ -24781,7 +23817,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_model", tags: ["graph:step:21"], run_id: "id-51", - metadata: shared71, + metadata: shared70, parent_ids: ["id-45"], }, }, @@ -24944,7 +23980,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-51", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:21"], - metadata: shared71, + metadata: shared70, data: { chunk: null, }, @@ -25111,7 +24147,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-51", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:21"], - metadata: shared71, + metadata: shared70, parent_ids: ["id-45"], }, }, @@ -25283,7 +24319,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:22"], run_id: "id-53", - metadata: shared72, + metadata: shared71, parent_ids: ["id-45"], }, }, @@ -25446,7 +24482,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-53", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:22"], - metadata: shared72, + metadata: shared71, data: { chunk: null, }, @@ -25613,7 +24649,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-53", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:22"], - metadata: shared72, + metadata: shared71, parent_ids: ["id-45"], }, }, @@ -26109,26 +25145,26 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { role: "user", content: "Hey there", }, - shared107, - shared121, + shared106, + shared113, { id: "id-17", role: "user", content: "My favorite fruit is Mango", }, - shared119, + shared111, { id: "id-30", role: "user", content: "and I love listening to Kaavish", }, - shared118, + shared110, { id: "id-43", role: "user", content: "tell me an interesting fact about Moon", }, - shared115, + shared107, ], }, { @@ -26292,8 +25328,8 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { ], }, }, - messages: [shared123], - tools: shared101, + messages: [shared115], + tools: shared100, context: [ { description: "Name of the user", @@ -26479,7 +25515,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:25"], run_id: "id-57", - metadata: shared62, + metadata: shared61, parent_ids: ["id-58"], }, }, @@ -26873,304 +25909,127 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, response_metadata: { finish_reason: "stop", - model_name: "gpt-4.1-mini", - model_provider: "openai", - }, - type: "ai", - name: null, - id: "id-24", - tool_calls: [], - invalid_tool_calls: [], - usage_metadata: null, - }, - { - content: "and I love listening to Kaavish", - additional_kwargs: { - type: "human", - }, - response_metadata: {}, - type: "human", - name: null, - id: "id-30", - }, - { - content: - "Kaavish is a wonderful musical group known for their unique blend of Eastern and Western sounds!", - additional_kwargs: { - invalid_tool_calls: [], - usage_metadata: null, - }, - response_metadata: { - finish_reason: "stop", - model_name: "gpt-4.1-mini", - model_provider: "openai", - }, - type: "ai", - name: null, - id: "id-37", - tool_calls: [], - invalid_tool_calls: [], - usage_metadata: null, - }, - { - content: "tell me an interesting fact about Moon", - additional_kwargs: { - type: "human", - }, - response_metadata: {}, - type: "human", - name: null, - id: "id-43", - }, - { - content: - "The Moon is Earth's only natural satellite, orbiting at an average distance of about 384,400 km. It takes approximately 27.3 days to complete one orbit.", - additional_kwargs: { - invalid_tool_calls: [], - usage_metadata: null, - }, - response_metadata: { - finish_reason: "stop", - model_name: "gpt-4.1-mini", - model_provider: "openai", - }, - type: "ai", - name: null, - id: "id-50", - tool_calls: [], - invalid_tool_calls: [], - usage_metadata: null, - }, - { - content: "Can you remind me what my favorite fruit is?", - additional_kwargs: { - type: "human", - }, - response_metadata: {}, - type: "human", - name: null, - id: "id-56", - }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], - }, - }, - }, - run_id: "id-57", - name: "CopilotKitMiddleware.before_agent", - tags: ["graph:step:25"], - metadata: shared62, - parent_ids: ["id-58"], - }, - }, - }, - { - type: "STEP_FINISHED", - stepName: "CopilotKitMiddleware.before_agent", - }, - { - type: "STEP_STARTED", - stepName: "model", - }, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - content: "Hey there", - additional_kwargs: { - type: "human", - }, - response_metadata: {}, - type: "human", - name: null, - id: "id-3", - }, - { - content: - 'App Context:\n{\n "copilotkit_forwarded_headers": {\n "x-forwarded-for": "",\n "x-forwarded-host": "",\n "x-forwarded-port": "",\n "x-forwarded-proto": ""\n }\n}', - additional_kwargs: {}, - response_metadata: {}, - type: "system", - name: null, - id: "id-7", - }, - { - content: "Hello! How can I assist you today?", - additional_kwargs: { - invalid_tool_calls: [], - usage_metadata: null, - }, - response_metadata: { - finish_reason: "stop", - model_name: "gpt-4.1-mini", - model_provider: "openai", - }, - type: "ai", - name: null, - id: "id-11", - tool_calls: [], - invalid_tool_calls: [], - usage_metadata: null, - }, - { - content: "My favorite fruit is Mango", - additional_kwargs: { - type: "human", - }, - response_metadata: {}, - type: "human", - name: null, - id: "id-17", - }, - { - content: - "That's great! Mango is a wonderful tropical fruit known for its sweet, juicy flavor.", - additional_kwargs: { - invalid_tool_calls: [], - usage_metadata: null, - }, - response_metadata: { - finish_reason: "stop", - model_name: "gpt-4.1-mini", - model_provider: "openai", - }, - type: "ai", - name: null, - id: "id-24", - tool_calls: [], - invalid_tool_calls: [], - usage_metadata: null, - }, - { - content: "and I love listening to Kaavish", - additional_kwargs: { - type: "human", - }, - response_metadata: {}, - type: "human", - name: null, - id: "id-30", - }, - { - content: - "Kaavish is a wonderful musical group known for their unique blend of Eastern and Western sounds!", - additional_kwargs: { - invalid_tool_calls: [], - usage_metadata: null, - }, - response_metadata: { - finish_reason: "stop", - model_name: "gpt-4.1-mini", - model_provider: "openai", - }, - type: "ai", - name: null, - id: "id-37", - tool_calls: [], - invalid_tool_calls: [], - usage_metadata: null, - }, - { - content: "tell me an interesting fact about Moon", - additional_kwargs: { - type: "human", - }, - response_metadata: {}, - type: "human", - name: null, - id: "id-43", - }, - { - content: - "The Moon is Earth's only natural satellite, orbiting at an average distance of about 384,400 km. It takes approximately 27.3 days to complete one orbit.", - additional_kwargs: { - invalid_tool_calls: [], - usage_metadata: null, - }, - response_metadata: { - finish_reason: "stop", - model_name: "gpt-4.1-mini", - model_provider: "openai", - }, - type: "ai", - name: null, - id: "id-50", - tool_calls: [], - invalid_tool_calls: [], - usage_metadata: null, - }, - { - content: "Can you remind me what my favorite fruit is?", - additional_kwargs: { - type: "human", - }, - response_metadata: {}, - type: "human", - name: null, - id: "id-56", - }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", + model_name: "gpt-4.1-mini", + model_provider: "openai", + }, + type: "ai", + name: null, + id: "id-24", + tool_calls: [], + invalid_tool_calls: [], + usage_metadata: null, + }, + { + content: "and I love listening to Kaavish", + additional_kwargs: { + type: "human", + }, + response_metadata: {}, + type: "human", + name: null, + id: "id-30", + }, + { + content: + "Kaavish is a wonderful musical group known for their unique blend of Eastern and Western sounds!", + additional_kwargs: { + invalid_tool_calls: [], + usage_metadata: null, + }, + response_metadata: { + finish_reason: "stop", + model_name: "gpt-4.1-mini", + model_provider: "openai", + }, + type: "ai", + name: null, + id: "id-37", + tool_calls: [], + invalid_tool_calls: [], + usage_metadata: null, + }, + { + content: "tell me an interesting fact about Moon", + additional_kwargs: { + type: "human", + }, + response_metadata: {}, + type: "human", + name: null, + id: "id-43", + }, + { + content: + "The Moon is Earth's only natural satellite, orbiting at an average distance of about 384,400 km. It takes approximately 27.3 days to complete one orbit.", + additional_kwargs: { + invalid_tool_calls: [], + usage_metadata: null, + }, + response_metadata: { + finish_reason: "stop", + model_name: "gpt-4.1-mini", + model_provider: "openai", + }, + type: "ai", + name: null, + id: "id-50", + tool_calls: [], + invalid_tool_calls: [], + usage_metadata: null, + }, + { + content: "Can you remind me what my favorite fruit is?", + additional_kwargs: { + type: "human", + }, + response_metadata: {}, + type: "human", + name: null, + id: "id-56", + }, + ], + copilotkit: { + actions: [ + { + type: "function", + name: "change_background", + function: { + name: "change_background", description: - "The background. Prefer gradients. Only use when asked.", + "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", + parameters: { + type: "object", + properties: { + background: { + type: "string", + description: + "The background. Prefer gradients. Only use when asked.", + }, + }, + required: ["background"], + }, }, }, - required: ["background"], - }, + ], }, }, - ], - }, - }, - rawEvent: { - event: "events", - data: { - event: "on_chain_start", - data: shared3, - name: "model", - tags: ["graph:step:26"], - run_id: "id-60", - metadata: shared91, + }, + run_id: "id-57", + name: "CopilotKitMiddleware.before_agent", + tags: ["graph:step:25"], + metadata: shared61, parent_ids: ["id-58"], }, }, }, + { + type: "STEP_FINISHED", + stepName: "CopilotKitMiddleware.before_agent", + }, + { + type: "STEP_STARTED", + stepName: "model", + }, { type: "STATE_SNAPSHOT", snapshot: { @@ -27338,42 +26197,13 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { rawEvent: { event: "events", data: { - event: "on_chat_model_start", - data: { - input: shared8, - }, - name: "ChatOpenAI", - tags: ["seq:step:1"], - run_id: "id-62", - metadata: { - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_api_version: "", - langgraph_plan: "enterprise", - langgraph_host: "self-hosted", - langgraph_api_url: "", - run_id: "id-58", - thread_id: "id-1", - langgraph_step: 26, - langgraph_node: "model", - langgraph_triggers: ["branch:to:model"], - langgraph_path: ["__pregel_pull", "model"], - langgraph_checkpoint_ns: "model:id-61", - checkpoint_ns: "model:id-61", - ls_provider: "openai", - ls_model_name: "gpt-4.1-mini", - ls_model_type: "chat", - ls_temperature: null, - lc_versions: { - "langchain-core": "1.5.3", - langchain: "1.3.14", - }, - }, - parent_ids: ["id-58", "id-60"], + event: "on_chain_start", + data: shared3, + name: "model", + tags: ["graph:step:26"], + run_id: "id-60", + metadata: shared90, + parent_ids: ["id-58"], }, }, }, @@ -27544,13 +26374,13 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { rawEvent: { event: "events", data: { - event: "on_chat_model_stream", + event: "on_chat_model_start", data: { - chunk: shared114, + input: shared8, }, - run_id: "id-62", name: "ChatOpenAI", tags: ["seq:step:1"], + run_id: "id-62", metadata: { ls_integration: "langchain_chat_model", created_by: "system", @@ -27748,10 +26578,30 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, }, rawEvent: { - event: "messages", - data: [ - shared114, - { + event: "events", + data: { + event: "on_chat_model_stream", + data: { + chunk: { + content: "", + additional_kwargs: {}, + response_metadata: { + model_provider: "openai", + }, + type: "AIMessageChunk", + name: null, + id: "id-63", + tool_calls: [], + invalid_tool_calls: [], + usage_metadata: null, + tool_call_chunks: [], + chunk_position: null, + }, + }, + run_id: "id-62", + name: "ChatOpenAI", + tags: ["seq:step:1"], + metadata: { ls_integration: "langchain_chat_model", created_by: "system", graph_id: "agentic_chat", @@ -27779,7 +26629,8 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { langchain: "1.3.14", }, }, - ], + parent_ids: ["id-58", "id-60"], + }, }, }, { @@ -28248,9 +27099,9 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-60", name: "model", tags: ["graph:step:26"], - metadata: shared91, + metadata: shared90, data: { - chunk: shared106, + chunk: shared105, }, parent_ids: ["id-58"], }, @@ -28425,7 +27276,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { data: { event: "on_chain_end", data: { - output: shared106, + output: shared105, input: { messages: [ { @@ -28592,7 +27443,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-60", name: "model", tags: ["graph:step:26"], - metadata: shared91, + metadata: shared90, parent_ids: ["id-58"], }, }, @@ -28792,7 +27643,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_model", tags: ["graph:step:27"], run_id: "id-64", - metadata: shared75, + metadata: shared74, parent_ids: ["id-58"], }, }, @@ -28983,7 +27834,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-64", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:27"], - metadata: shared75, + metadata: shared74, data: { chunk: null, }, @@ -29178,7 +28029,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-64", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:27"], - metadata: shared75, + metadata: shared74, parent_ids: ["id-58"], }, }, @@ -29378,7 +28229,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:28"], run_id: "id-66", - metadata: shared76, + metadata: shared75, parent_ids: ["id-58"], }, }, @@ -29569,7 +28420,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-66", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:28"], - metadata: shared76, + metadata: shared75, data: { chunk: null, }, @@ -29764,7 +28615,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-66", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:28"], - metadata: shared76, + metadata: shared75, parent_ids: ["id-58"], }, }, @@ -30344,27 +29195,27 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { role: "user", content: "Hey there", }, - shared107, - shared121, + shared106, + shared113, { id: "id-17", role: "user", content: "My favorite fruit is Mango", }, - shared119, + shared111, { id: "id-30", role: "user", content: "and I love listening to Kaavish", }, - shared118, + shared110, { id: "id-43", role: "user", content: "tell me an interesting fact about Moon", }, + shared107, shared115, - shared123, { id: "id-63", role: "assistant", @@ -30395,7 +29246,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { content: "Hi, I am duaa", }, ], - tools: shared101, + tools: shared100, context: [ { description: "Name of the user", @@ -30411,13 +29262,13 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared85, + snapshot: shared84, rawEvent: { event: "events", data: { event: "on_chain_start", data: { - input: shared85, + input: shared84, }, name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:1"], @@ -30450,14 +29301,14 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared51, + snapshot: shared50, rawEvent: { event: "events", data: { event: "on_chain_end", data: { - output: shared51, - input: shared85, + output: shared50, + input: shared84, }, run_id: "id-4", name: "CopilotKitMiddleware.before_agent", @@ -30490,7 +29341,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared51, + snapshot: shared50, rawEvent: shared46, }, { @@ -30503,31 +29354,31 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared50, + snapshot: shared49, rawEvent: { event: "events", data: { event: "on_chain_start", data: { - input: shared50, + input: shared49, }, name: "model", tags: ["graph:step:2"], run_id: "id-8", - metadata: shared94, + metadata: shared93, parent_ids: ["id-5"], }, }, }, { type: "STATE_SNAPSHOT", - snapshot: shared50, + snapshot: shared49, rawEvent: { event: "events", data: { event: "on_chat_model_start", data: { - input: shared82, + input: shared81, }, name: "ChatOpenAI", tags: ["seq:step:1"], @@ -30566,17 +29417,12 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared50, + snapshot: shared49, rawEvent: shared36, }, { type: "STATE_SNAPSHOT", - snapshot: shared50, - rawEvent: shared47, - }, - { - type: "STATE_SNAPSHOT", - snapshot: shared50, + snapshot: shared49, rawEvent: { event: "events", data: shared43, @@ -30672,7 +29518,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { invalid_tool_calls: [], usage_metadata: null, }, - input: shared82, + input: shared81, }, run_id: "id-10", name: "ChatOpenAI", @@ -30710,7 +29556,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared50, + snapshot: shared49, rawEvent: { event: "events", data: { @@ -30718,9 +29564,9 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-8", name: "model", tags: ["graph:step:2"], - metadata: shared94, + metadata: shared93, data: { - chunk: shared102, + chunk: shared101, }, parent_ids: ["id-5"], }, @@ -30728,19 +29574,19 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared50, + snapshot: shared49, rawEvent: { event: "events", data: { event: "on_chain_end", data: { - output: shared102, - input: shared50, + output: shared101, + input: shared49, }, run_id: "id-8", name: "model", tags: ["graph:step:2"], - metadata: shared94, + metadata: shared93, parent_ids: ["id-5"], }, }, @@ -30915,7 +29761,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { ], }, }, - rawEvent: shared55, + rawEvent: shared54, }, { type: "STATE_SNAPSHOT", @@ -31185,7 +30031,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { ], }, }, - rawEvent: shared56, + rawEvent: shared55, }, { type: "STATE_SNAPSHOT", @@ -31420,7 +30266,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-5", name: "agentic_chat", tags: [], - metadata: shared99, + metadata: shared98, parent_ids: [], }, }, @@ -31502,7 +30348,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { role: "user", content: "Hi, I am duaa", }, - shared107, + shared106, { id: "id-11", role: "assistant", diff --git a/apps/dojo/e2e/tests/langgraphPythonTests/sharedStatePage.spec.ts b/apps/dojo/e2e/tests/langgraphPythonTests/sharedStatePage.spec.ts index d7e7d5949a..29efd0b64a 100644 --- a/apps/dojo/e2e/tests/langgraphPythonTests/sharedStatePage.spec.ts +++ b/apps/dojo/e2e/tests/langgraphPythonTests/sharedStatePage.spec.ts @@ -29,6 +29,9 @@ test.describe("Shared State Feature", () => { await page.goto("/langgraph/feature/shared_state"); await sharedStateAgent.openChat(); + // Wait for the runtime-backed agent to replace the provisional agent before + // editing shared state, so the graph observes the same recipe as the UI. + await sharedStateAgent.awaitAgentReady(); // Add new ingredient via UI await sharedStateAgent.addIngredient.click(); @@ -38,9 +41,6 @@ test.describe("Shared State Feature", () => { await newIngredientCard.locator(".ingredient-name-input").fill("Potatoes"); await newIngredientCard.locator(".ingredient-amount-input").fill("12"); - // Wait for UI to update - await page.waitForTimeout(1000); - // Ask chat for all ingredients await sharedStateAgent.sendMessage("Give me all the ingredients"); await sharedStateAgent.loader(); diff --git a/apps/dojo/e2e/tests/langgraphTypescriptTests/agenticChatPage.event-trace.ts b/apps/dojo/e2e/tests/langgraphTypescriptTests/agenticChatPage.event-trace.ts index 95dbedce8b..500abf44d2 100644 --- a/apps/dojo/e2e/tests/langgraphTypescriptTests/agenticChatPage.event-trace.ts +++ b/apps/dojo/e2e/tests/langgraphTypescriptTests/agenticChatPage.event-trace.ts @@ -1,7 +1,7 @@ import { defineEventTrace } from "../../event-trace-test"; // Generated by the event trace updater. -// Reason: Normalize LangGraph dependency versions so event traces remain stable across supported lanes. +// Reason: Ignore late LangGraph messages tuples once events mode is active so obsolete snapshots are not emitted. // Before accepting changes, verify whether the implementation regressed. const shared1 = { output: { @@ -5845,96 +5845,6 @@ const shared56 = { }, } as const; const shared57 = { - id: "10", - event: "messages", - data: [ - { - content: "", - tool_call_chunks: [], - additional_kwargs: {}, - id: "id-10", - response_metadata: { - model_provider: "openai", - usage: {}, - }, - tool_calls: [], - invalid_tool_calls: [], - type: "ai", - }, - { - tags: [], - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_plan: "developer", - langgraph_host: "self-hosted", - langgraph_api_url: "", - thread_id: "id-1", - run_id: "id-5", - langgraph_step: 2, - langgraph_node: "model_request", - langgraph_triggers: ["branch:to:model_request"], - langgraph_path: ["__pregel_pull", "model_request"], - langgraph_checkpoint_ns: "model_request:id-8", - checkpoint_ns: "model_request:id-8", - ls_provider: "openai", - ls_model_name: "gpt-4o", - ls_model_type: "chat", - versions: { - "@langchain/core": "1.1.46", - }, - }, - ], -} as const; -const shared58 = { - id: "16", - event: "messages", - data: [ - { - content: "", - tool_call_chunks: [], - additional_kwargs: {}, - id: "id-10", - response_metadata: { - model_provider: "openai", - usage: {}, - }, - tool_calls: [], - invalid_tool_calls: [], - type: "ai", - }, - { - tags: [], - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_plan: "developer", - langgraph_host: "self-hosted", - langgraph_api_url: "", - thread_id: "id-1", - run_id: "id-5", - langgraph_step: 2, - langgraph_node: "model_request", - langgraph_triggers: ["branch:to:model_request"], - langgraph_path: ["__pregel_pull", "model_request"], - langgraph_checkpoint_ns: "model_request:id-8", - checkpoint_ns: "model_request:id-8", - ls_provider: "openai", - ls_model_name: "gpt-4o", - ls_model_type: "chat", - versions: { - "@langchain/core": "1.1.46", - }, - }, - ], -} as const; -const shared59 = { input: { messages: [ { @@ -5991,7 +5901,7 @@ const shared59 = { }, }, } as const; -const shared60 = { +const shared58 = { messages: [ { content: "Hello duaa! How can I assist you today?", @@ -6019,319 +5929,32 @@ const shared60 = { type: "ai", }, ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], - }, -} as const; -const shared61 = [ - { - content: "", - tool_call_chunks: [], - additional_kwargs: {}, - id: "id-38", - response_metadata: { - model_provider: "openai", - usage: {}, - }, - tool_calls: [], - invalid_tool_calls: [], - type: "ai", - }, - { - tags: [], - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_plan: "developer", - langgraph_host: "self-hosted", - langgraph_api_url: "", - thread_id: "id-1", - run_id: "id-33", - langgraph_step: 14, - langgraph_node: "model_request", - langgraph_triggers: ["branch:to:model_request"], - langgraph_path: ["__pregel_pull", "model_request"], - langgraph_checkpoint_ns: "model_request:id-36", - checkpoint_ns: "model_request:id-36", - ls_provider: "openai", - ls_model_name: "gpt-4o", - ls_model_type: "chat", - versions: { - "@langchain/core": "1.1.46", - }, - }, -] as const; -const shared62 = [ - { - content: "", - tool_call_chunks: [], - additional_kwargs: {}, - id: "id-39", - response_metadata: { - model_provider: "openai", - usage: {}, - }, - tool_calls: [], - invalid_tool_calls: [], - type: "ai", - }, - { - tags: [], - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_plan: "developer", - langgraph_host: "self-hosted", - langgraph_api_url: "", - thread_id: "id-1", - run_id: "id-34", - langgraph_step: 14, - langgraph_node: "model_request", - langgraph_triggers: ["branch:to:model_request"], - langgraph_path: ["__pregel_pull", "model_request"], - langgraph_checkpoint_ns: "model_request:id-37", - checkpoint_ns: "model_request:id-37", - ls_provider: "openai", - ls_model_name: "gpt-4o", - ls_model_type: "chat", - versions: { - "@langchain/core": "1.1.46", - }, - }, -] as const; -const shared63 = [ - { - content: "", - tool_call_chunks: [], - additional_kwargs: {}, - id: "id-52", - response_metadata: { - model_provider: "openai", - usage: {}, - }, - tool_calls: [], - invalid_tool_calls: [], - type: "ai", - }, - { - tags: [], - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_plan: "developer", - langgraph_host: "self-hosted", - langgraph_api_url: "", - thread_id: "id-1", - run_id: "id-47", - langgraph_step: 20, - langgraph_node: "model_request", - langgraph_triggers: ["branch:to:model_request"], - langgraph_path: ["__pregel_pull", "model_request"], - langgraph_checkpoint_ns: "model_request:id-50", - checkpoint_ns: "model_request:id-50", - ls_provider: "openai", - ls_model_name: "gpt-4o", - ls_model_type: "chat", - versions: { - "@langchain/core": "1.1.46", - }, - }, -] as const; -const shared64 = [ - { - content: "", - tool_call_chunks: [], - additional_kwargs: {}, - id: "id-54", - response_metadata: { - model_provider: "openai", - usage: {}, - }, - tool_calls: [], - invalid_tool_calls: [], - type: "ai", - }, - { - tags: [], - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_plan: "developer", - langgraph_host: "self-hosted", - langgraph_api_url: "", - thread_id: "id-1", - run_id: "id-49", - langgraph_step: 20, - langgraph_node: "model_request", - langgraph_triggers: ["branch:to:model_request"], - langgraph_path: ["__pregel_pull", "model_request"], - langgraph_checkpoint_ns: "model_request:id-52", - checkpoint_ns: "model_request:id-52", - ls_provider: "openai", - ls_model_name: "gpt-4o", - ls_model_type: "chat", - versions: { - "@langchain/core": "1.1.46", - }, - }, -] as const; -const shared65 = [ - { - content: "", - tool_call_chunks: [], - additional_kwargs: {}, - id: "id-66", - response_metadata: { - model_provider: "openai", - usage: {}, - }, - tool_calls: [], - invalid_tool_calls: [], - type: "ai", - }, - { - tags: [], - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_plan: "developer", - langgraph_host: "self-hosted", - langgraph_api_url: "", - thread_id: "id-1", - run_id: "id-61", - langgraph_step: 26, - langgraph_node: "model_request", - langgraph_triggers: ["branch:to:model_request"], - langgraph_path: ["__pregel_pull", "model_request"], - langgraph_checkpoint_ns: "model_request:id-64", - checkpoint_ns: "model_request:id-64", - ls_provider: "openai", - ls_model_name: "gpt-4o", - ls_model_type: "chat", - versions: { - "@langchain/core": "1.1.46", - }, - }, -] as const; -const shared66 = [ - { - content: "", - tool_call_chunks: [], - additional_kwargs: {}, - id: "id-24", - response_metadata: { - model_provider: "openai", - usage: {}, - }, - tool_calls: [], - invalid_tool_calls: [], - type: "ai", - }, - { - tags: [], - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_plan: "developer", - langgraph_host: "self-hosted", - langgraph_api_url: "", - thread_id: "id-1", - run_id: "id-19", - langgraph_step: 8, - langgraph_node: "model_request", - langgraph_triggers: ["branch:to:model_request"], - langgraph_path: ["__pregel_pull", "model_request"], - langgraph_checkpoint_ns: "model_request:id-22", - checkpoint_ns: "model_request:id-22", - ls_provider: "openai", - ls_model_name: "gpt-4o", - ls_model_type: "chat", - versions: { - "@langchain/core": "1.1.46", - }, - }, -] as const; -const shared67 = [ - { - content: "", - tool_call_chunks: [], - additional_kwargs: {}, - id: "id-25", - response_metadata: { - model_provider: "openai", - usage: {}, - }, - tool_calls: [], - invalid_tool_calls: [], - type: "ai", - }, - { - tags: [], - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_plan: "developer", - langgraph_host: "self-hosted", - langgraph_api_url: "", - thread_id: "id-1", - run_id: "id-20", - langgraph_step: 8, - langgraph_node: "model_request", - langgraph_triggers: ["branch:to:model_request"], - langgraph_path: ["__pregel_pull", "model_request"], - langgraph_checkpoint_ns: "model_request:id-23", - checkpoint_ns: "model_request:id-23", - ls_provider: "openai", - ls_model_name: "gpt-4o", - ls_model_type: "chat", - versions: { - "@langchain/core": "1.1.46", - }, + copilotkit: { + actions: [ + { + type: "function", + name: "change_background", + function: { + name: "change_background", + description: + "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", + parameters: { + type: "object", + properties: { + background: { + type: "string", + description: + "The background. Prefer gradients. Only use when asked.", + }, + }, + required: ["background"], + }, + }, + }, + ], }, -] as const; -const shared68 = { +} as const; +const shared59 = { messages: [ { content: "Done! I've completed that for you.", @@ -6384,7 +6007,7 @@ const shared68 = { ], }, } as const; -const shared69 = { +const shared60 = { messages: [ { content: "Done! I've completed that for you.", @@ -6437,7 +6060,7 @@ const shared69 = { ], }, } as const; -const shared70 = { +const shared61 = { messages: [ { content: "Hello! How can I assist you today?", @@ -6490,7 +6113,7 @@ const shared70 = { ], }, } as const; -const shared71 = { +const shared62 = { messages: [ { content: "Your favorite fruit is Mango!", @@ -6543,7 +6166,7 @@ const shared71 = { ], }, } as const; -const shared72 = { +const shared63 = { messages: [ { id: "id-3", @@ -6596,7 +6219,7 @@ const shared72 = { ], }, } as const; -const shared73 = { +const shared64 = { messages: [ [ { @@ -6652,7 +6275,7 @@ const shared73 = { ], ], } as const; -const shared74 = { +const shared65 = { messages: [ [ { @@ -6703,7 +6326,7 @@ const shared74 = { ], ], } as const; -const shared75 = { +const shared66 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -6722,7 +6345,7 @@ const shared75 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.before_agent:id-34", checkpoint_ns: "CopilotKitMiddleware.before_agent:id-34", } as const; -const shared76 = { +const shared67 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -6741,7 +6364,7 @@ const shared76 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.before_agent:id-35", checkpoint_ns: "CopilotKitMiddleware.before_agent:id-35", } as const; -const shared77 = { +const shared68 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -6760,7 +6383,7 @@ const shared77 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.before_agent:id-48", checkpoint_ns: "CopilotKitMiddleware.before_agent:id-48", } as const; -const shared78 = { +const shared69 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -6779,7 +6402,7 @@ const shared78 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.before_agent:id-50", checkpoint_ns: "CopilotKitMiddleware.before_agent:id-50", } as const; -const shared79 = { +const shared70 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -6798,7 +6421,7 @@ const shared79 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.before_agent:id-62", checkpoint_ns: "CopilotKitMiddleware.before_agent:id-62", } as const; -const shared80 = { +const shared71 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -6817,7 +6440,7 @@ const shared80 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.before_agent:id-20", checkpoint_ns: "CopilotKitMiddleware.before_agent:id-20", } as const; -const shared81 = { +const shared72 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -6836,7 +6459,7 @@ const shared81 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.before_agent:id-21", checkpoint_ns: "CopilotKitMiddleware.before_agent:id-21", } as const; -const shared82 = { +const shared73 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -6855,7 +6478,7 @@ const shared82 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.before_agent:id-6", checkpoint_ns: "CopilotKitMiddleware.before_agent:id-6", } as const; -const shared83 = { +const shared74 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -6874,7 +6497,7 @@ const shared83 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.after_model:id-40", checkpoint_ns: "CopilotKitMiddleware.after_model:id-40", } as const; -const shared84 = { +const shared75 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -6893,7 +6516,7 @@ const shared84 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.after_model:id-42", checkpoint_ns: "CopilotKitMiddleware.after_model:id-42", } as const; -const shared85 = { +const shared76 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -6912,7 +6535,7 @@ const shared85 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.after_model:id-54", checkpoint_ns: "CopilotKitMiddleware.after_model:id-54", } as const; -const shared86 = { +const shared77 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -6931,7 +6554,7 @@ const shared86 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.after_model:id-56", checkpoint_ns: "CopilotKitMiddleware.after_model:id-56", } as const; -const shared87 = { +const shared78 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -6950,7 +6573,7 @@ const shared87 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.after_model:id-68", checkpoint_ns: "CopilotKitMiddleware.after_model:id-68", } as const; -const shared88 = { +const shared79 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -6969,7 +6592,7 @@ const shared88 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.after_agent:id-29", checkpoint_ns: "CopilotKitMiddleware.after_agent:id-29", } as const; -const shared89 = { +const shared80 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -6988,7 +6611,7 @@ const shared89 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.after_agent:id-30", checkpoint_ns: "CopilotKitMiddleware.after_agent:id-30", } as const; -const shared90 = { +const shared81 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -7007,7 +6630,7 @@ const shared90 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.after_agent:id-43", checkpoint_ns: "CopilotKitMiddleware.after_agent:id-43", } as const; -const shared91 = { +const shared82 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -7026,7 +6649,7 @@ const shared91 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.after_agent:id-45", checkpoint_ns: "CopilotKitMiddleware.after_agent:id-45", } as const; -const shared92 = { +const shared83 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -7045,7 +6668,7 @@ const shared92 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.after_agent:id-57", checkpoint_ns: "CopilotKitMiddleware.after_agent:id-57", } as const; -const shared93 = { +const shared84 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -7064,7 +6687,7 @@ const shared93 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.after_agent:id-59", checkpoint_ns: "CopilotKitMiddleware.after_agent:id-59", } as const; -const shared94 = { +const shared85 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -7083,7 +6706,7 @@ const shared94 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.after_agent:id-71", checkpoint_ns: "CopilotKitMiddleware.after_agent:id-71", } as const; -const shared95 = { +const shared86 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -7102,7 +6725,7 @@ const shared95 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.after_model:id-26", checkpoint_ns: "CopilotKitMiddleware.after_model:id-26", } as const; -const shared96 = { +const shared87 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -7121,7 +6744,7 @@ const shared96 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.after_model:id-27", checkpoint_ns: "CopilotKitMiddleware.after_model:id-27", } as const; -const shared97 = { +const shared88 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -7140,7 +6763,7 @@ const shared97 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.after_model:id-12", checkpoint_ns: "CopilotKitMiddleware.after_model:id-12", } as const; -const shared98 = { +const shared89 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -7159,7 +6782,7 @@ const shared98 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.after_model:id-13", checkpoint_ns: "CopilotKitMiddleware.after_model:id-13", } as const; -const shared99 = { +const shared90 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -7178,7 +6801,7 @@ const shared99 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.after_agent:id-15", checkpoint_ns: "CopilotKitMiddleware.after_agent:id-15", } as const; -const shared100 = { +const shared91 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -7197,7 +6820,7 @@ const shared100 = { langgraph_checkpoint_ns: "CopilotKitMiddleware.after_agent:id-16", checkpoint_ns: "CopilotKitMiddleware.after_agent:id-16", } as const; -const shared101 = { +const shared92 = { input: { messages: [ { @@ -7234,7 +6857,7 @@ const shared101 = { }, }, } as const; -const shared102 = { +const shared93 = { input: { messages: [ { @@ -7271,7 +6894,7 @@ const shared102 = { }, }, } as const; -const shared103 = { +const shared94 = { input: { messages: [ { @@ -7308,7 +6931,7 @@ const shared103 = { }, }, } as const; -const shared104 = { +const shared95 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -7327,7 +6950,7 @@ const shared104 = { langgraph_checkpoint_ns: "model_request:id-36", checkpoint_ns: "model_request:id-36", } as const; -const shared105 = { +const shared96 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -7346,7 +6969,7 @@ const shared105 = { langgraph_checkpoint_ns: "model_request:id-37", checkpoint_ns: "model_request:id-37", } as const; -const shared106 = { +const shared97 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -7365,7 +6988,7 @@ const shared106 = { langgraph_checkpoint_ns: "model_request:id-50", checkpoint_ns: "model_request:id-50", } as const; -const shared107 = { +const shared98 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -7384,7 +7007,7 @@ const shared107 = { langgraph_checkpoint_ns: "model_request:id-52", checkpoint_ns: "model_request:id-52", } as const; -const shared108 = { +const shared99 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -7403,7 +7026,7 @@ const shared108 = { langgraph_checkpoint_ns: "model_request:id-64", checkpoint_ns: "model_request:id-64", } as const; -const shared109 = { +const shared100 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -7422,7 +7045,7 @@ const shared109 = { langgraph_checkpoint_ns: "model_request:id-22", checkpoint_ns: "model_request:id-22", } as const; -const shared110 = { +const shared101 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -7441,7 +7064,7 @@ const shared110 = { langgraph_checkpoint_ns: "model_request:id-23", checkpoint_ns: "model_request:id-23", } as const; -const shared111 = { +const shared102 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -7460,7 +7083,7 @@ const shared111 = { langgraph_checkpoint_ns: "model_request:id-8", checkpoint_ns: "model_request:id-8", } as const; -const shared112 = [ +const shared103 = [ { name: "change_background", description: @@ -7477,7 +7100,7 @@ const shared112 = [ }, }, ] as const; -const shared113 = { +const shared104 = { ls_integration: "langgraph", created_by: "system", graph_id: "agentic_chat", @@ -7490,7 +7113,7 @@ const shared113 = { thread_id: "id-1", run_id: "id-5", } as const; -const shared114 = { +const shared105 = { messages: [ [ { @@ -7514,7 +7137,7 @@ const shared114 = { ], ], } as const; -const shared115 = { +const shared106 = { messages: [ [ { @@ -7538,7 +7161,7 @@ const shared115 = { ], ], } as const; -const shared116 = { +const shared107 = { messages: [ [ { @@ -7562,14 +7185,14 @@ const shared116 = { ], ], } as const; -const shared117 = { +const shared108 = { id: "id-52", role: "assistant", content: "The Moon is Earth's only natural satellite, orbiting at an average distance of about 384,400 km. It takes approximately 27.3 days to complete one orbit.", toolCalls: [], } as const; -const shared118 = { +const shared109 = { id: "id-10", role: "assistant", content: "", @@ -7584,7 +7207,7 @@ const shared118 = { }, ], } as const; -const shared119 = { +const shared110 = { id: "id-39", role: "assistant", content: "", @@ -7599,39 +7222,39 @@ const shared119 = { }, ], } as const; -const shared120 = { +const shared111 = { id: "id-38", role: "assistant", content: "Kaavish is a wonderful musical group known for their unique blend of Eastern and Western sounds!", toolCalls: [], } as const; -const shared121 = { +const shared112 = { id: "id-24", role: "assistant", content: "That's great! Mango is a wonderful tropical fruit known for its sweet, juicy flavor.", toolCalls: [], } as const; -const shared122 = { +const shared113 = { id: "id-18", role: "tool", content: '{"status":"success","message":"Background changed to blue"}', toolCallId: "id-11", } as const; -const shared123 = { +const shared114 = { id: "id-10", role: "assistant", content: "Hello! How can I assist you today?", toolCalls: [], } as const; -const shared124 = { +const shared115 = { id: "id-25", role: "assistant", content: "Done! I've completed that for you.", toolCalls: [], } as const; -const shared125 = { +const shared116 = { id: "id-59", role: "user", content: "Can you remind me what my favorite fruit is?", @@ -7654,7 +7277,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { content: "Hi change the background color to blue", }, ], - tools: shared112, + tools: shared103, context: [ { description: "Name of the user", @@ -7710,11 +7333,11 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { event: "events", data: { event: "on_chain_start", - data: shared101, + data: shared92, name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:1"], run_id: "id-4", - metadata: shared82, + metadata: shared73, }, }, }, @@ -7835,7 +7458,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-4", name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:1"], - metadata: shared82, + metadata: shared73, }, }, }, @@ -7889,11 +7512,11 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { event: "events", data: { event: "on_chain_start", - data: shared101, + data: shared92, name: "model_request", tags: ["graph:step:2"], run_id: "id-7", - metadata: shared111, + metadata: shared102, }, }, }, @@ -7940,7 +7563,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { data: { event: "on_chat_model_start", data: { - input: shared114, + input: shared105, }, name: "ChatOpenAI", tags: [], @@ -8012,45 +7635,6 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, rawEvent: shared36, }, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - id: "id-3", - content: "Hi change the background color to blue", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], - }, - }, - rawEvent: shared57, - }, { type: "STATE_SNAPSHOT", snapshot: { @@ -8237,108 +7821,23 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { langgraph_triggers: ["branch:to:model_request"], langgraph_path: ["__pregel_pull", "model_request"], langgraph_checkpoint_ns: "model_request:id-8", - checkpoint_ns: "model_request:id-8", - ls_provider: "openai", - ls_model_name: "gpt-4o", - ls_model_type: "chat", - versions: { - "@langchain/core": "1.1.46", - }, - }, - }, - }, - { - type: "TOOL_CALL_END", - toolCallId: "id-11", - rawEvent: { - event: "on_chat_model_stream", - data: { - chunk: { - content: "", - tool_call_chunks: [], - additional_kwargs: {}, - id: "id-10", - response_metadata: { - model_provider: "openai", - usage: {}, - }, - tool_calls: [], - invalid_tool_calls: [], - type: "ai", - }, - }, - run_id: "id-9", - name: "ChatOpenAI", - tags: [], - metadata: { - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_plan: "developer", - langgraph_host: "self-hosted", - langgraph_api_url: "", - thread_id: "id-1", - run_id: "id-5", - langgraph_step: 2, - langgraph_node: "model_request", - langgraph_triggers: ["branch:to:model_request"], - langgraph_path: ["__pregel_pull", "model_request"], - langgraph_checkpoint_ns: "model_request:id-8", - checkpoint_ns: "model_request:id-8", - ls_provider: "openai", - ls_model_name: "gpt-4o", - ls_model_type: "chat", - versions: { - "@langchain/core": "1.1.46", - }, - }, - }, - }, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - id: "id-3", - content: "Hi change the background color to blue", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], + checkpoint_ns: "model_request:id-8", + ls_provider: "openai", + ls_model_name: "gpt-4o", + ls_model_type: "chat", + versions: { + "@langchain/core": "1.1.46", + }, }, }, + }, + { + type: "TOOL_CALL_END", + toolCallId: "id-11", rawEvent: { - id: "18", - event: "messages", - data: [ - { + event: "on_chat_model_stream", + data: { + chunk: { content: "", tool_call_chunks: [], additional_kwargs: {}, @@ -8351,33 +7850,35 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { invalid_tool_calls: [], type: "ai", }, - { - tags: [], - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_plan: "developer", - langgraph_host: "self-hosted", - langgraph_api_url: "", - thread_id: "id-1", - run_id: "id-5", - langgraph_step: 2, - langgraph_node: "model_request", - langgraph_triggers: ["branch:to:model_request"], - langgraph_path: ["__pregel_pull", "model_request"], - langgraph_checkpoint_ns: "model_request:id-8", - checkpoint_ns: "model_request:id-8", - ls_provider: "openai", - ls_model_name: "gpt-4o", - ls_model_type: "chat", - versions: { - "@langchain/core": "1.1.46", - }, + }, + run_id: "id-9", + name: "ChatOpenAI", + tags: [], + metadata: { + ls_integration: "langchain_chat_model", + created_by: "system", + graph_id: "agentic_chat", + assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", + run_attempt: 1, + langgraph_version: "", + langgraph_plan: "developer", + langgraph_host: "self-hosted", + langgraph_api_url: "", + thread_id: "id-1", + run_id: "id-5", + langgraph_step: 2, + langgraph_node: "model_request", + langgraph_triggers: ["branch:to:model_request"], + langgraph_path: ["__pregel_pull", "model_request"], + langgraph_checkpoint_ns: "model_request:id-8", + checkpoint_ns: "model_request:id-8", + ls_provider: "openai", + ls_model_name: "gpt-4o", + ls_model_type: "chat", + versions: { + "@langchain/core": "1.1.46", }, - ], + }, }, }, { @@ -8476,7 +7977,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { invalid_tool_calls: [], type: "ai", }, - input: shared114, + input: shared105, }, run_id: "id-9", name: "ChatOpenAI", @@ -8615,7 +8116,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-7", name: "model_request", tags: ["graph:step:2"], - metadata: shared111, + metadata: shared102, }, }, }, @@ -8641,7 +8142,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_model", tags: ["graph:step:3"], run_id: "id-12", - metadata: shared98, + metadata: shared89, }, }, }, @@ -8653,11 +8154,11 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { event: "events", data: { event: "on_chain_start", - data: shared59, + data: shared57, name: "RunnableLambda", tags: [], run_id: "id-14", - metadata: shared98, + metadata: shared89, }, }, }, @@ -8730,7 +8231,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-14", name: "RunnableLambda", tags: [], - metadata: shared98, + metadata: shared89, }, }, }, @@ -8857,7 +8358,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-12", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:3"], - metadata: shared98, + metadata: shared89, }, }, }, @@ -8931,24 +8432,24 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { event: "events", data: { event: "on_chain_start", - data: shared59, + data: shared57, name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:4"], run_id: "id-15", - metadata: shared100, + metadata: shared91, }, }, }, { type: "STATE_SNAPSHOT", - snapshot: shared72, + snapshot: shared63, rawEvent: { id: "30", event: "events", data: { event: "on_chain_end", data: { - output: shared72, + output: shared63, input: { messages: [ { @@ -9008,25 +8509,25 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-15", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:4"], - metadata: shared100, + metadata: shared91, }, }, }, { type: "STATE_SNAPSHOT", - snapshot: shared72, + snapshot: shared63, rawEvent: { id: "33", event: "events", data: { event: "on_chain_end", data: { - output: shared72, + output: shared63, }, run_id: "id-5", name: "LangGraph", tags: [], - metadata: shared113, + metadata: shared104, }, }, }, @@ -9036,7 +8537,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared72, + snapshot: shared63, }, { type: "MESSAGES_SNAPSHOT", @@ -9046,7 +8547,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { role: "user", content: "Hi change the background color to blue", }, - shared118, + shared109, ], }, { @@ -9061,9 +8562,9 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { input: { threadId: "id-1", runId: "id-17", - state: shared72, + state: shared63, messages: [ - shared118, + shared109, { id: "id-18", content: @@ -9072,7 +8573,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { toolCallId: "id-11", }, ], - tools: shared112, + tools: shared103, context: [ { description: "Name of the user", @@ -9160,7 +8661,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:7"], run_id: "id-19", - metadata: shared81, + metadata: shared72, }, }, }, @@ -9365,7 +8866,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-19", name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:7"], - metadata: shared81, + metadata: shared72, }, }, }, @@ -9451,7 +8952,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "model_request", tags: ["graph:step:8"], run_id: "id-22", - metadata: shared110, + metadata: shared101, }, }, }, @@ -9526,7 +9027,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { data: { event: "on_chat_model_start", data: { - input: shared74, + input: shared65, }, name: "ChatOpenAI", tags: [], @@ -9630,77 +9131,6 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { data: shared54, }, }, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - id: "id-3", - content: "Hi change the background color to blue", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: "", - id: "id-10", - invalid_tool_calls: [], - additional_kwargs: {}, - response_metadata: {}, - tool_calls: [ - { - name: "change_background", - args: { - background: "blue", - }, - id: "id-11", - type: "tool_call", - }, - ], - type: "ai", - }, - { - content: - '{"status":"success","message":"Background changed to blue"}', - tool_call_id: "id-11", - id: "id-18", - status: "success", - additional_kwargs: {}, - response_metadata: {}, - type: "tool", - }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], - }, - }, - rawEvent: { - id: "10", - event: "messages", - data: shared67, - }, - }, { type: "STATE_SNAPSHOT", snapshot: { @@ -9797,119 +9227,48 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { additional_kwargs: {}, id: "id-25", response_metadata: { - model_provider: "openai", - usage: {}, - }, - tool_calls: [], - invalid_tool_calls: [], - type: "ai", - }, - }, - run_id: "id-24", - name: "ChatOpenAI", - tags: [], - metadata: { - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_plan: "developer", - langgraph_host: "self-hosted", - langgraph_api_url: "", - thread_id: "id-1", - run_id: "id-20", - langgraph_step: 8, - langgraph_node: "model_request", - langgraph_triggers: ["branch:to:model_request"], - langgraph_path: ["__pregel_pull", "model_request"], - langgraph_checkpoint_ns: "model_request:id-23", - checkpoint_ns: "model_request:id-23", - ls_provider: "openai", - ls_model_name: "gpt-4o", - ls_model_type: "chat", - versions: { - "@langchain/core": "1.1.46", - }, - }, - }, - }, - { - type: "TEXT_MESSAGE_END", - messageId: "id-25", - rawEvent: shared54, - }, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - id: "id-3", - content: "Hi change the background color to blue", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: "", - id: "id-10", - invalid_tool_calls: [], - additional_kwargs: {}, - response_metadata: {}, - tool_calls: [ - { - name: "change_background", - args: { - background: "blue", - }, - id: "id-11", - type: "tool_call", - }, - ], - type: "ai", - }, - { - content: - '{"status":"success","message":"Background changed to blue"}', - tool_call_id: "id-11", - id: "id-18", - status: "success", - additional_kwargs: {}, - response_metadata: {}, - type: "tool", - }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, + model_provider: "openai", + usage: {}, }, - ], + tool_calls: [], + invalid_tool_calls: [], + type: "ai", + }, + }, + run_id: "id-24", + name: "ChatOpenAI", + tags: [], + metadata: { + ls_integration: "langchain_chat_model", + created_by: "system", + graph_id: "agentic_chat", + assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", + run_attempt: 1, + langgraph_version: "", + langgraph_plan: "developer", + langgraph_host: "self-hosted", + langgraph_api_url: "", + thread_id: "id-1", + run_id: "id-20", + langgraph_step: 8, + langgraph_node: "model_request", + langgraph_triggers: ["branch:to:model_request"], + langgraph_path: ["__pregel_pull", "model_request"], + langgraph_checkpoint_ns: "model_request:id-23", + checkpoint_ns: "model_request:id-23", + ls_provider: "openai", + ls_model_name: "gpt-4o", + ls_model_type: "chat", + versions: { + "@langchain/core": "1.1.46", + }, }, }, - rawEvent: { - id: "16", - event: "messages", - data: shared67, - }, + }, + { + type: "TEXT_MESSAGE_END", + messageId: "id-25", + rawEvent: shared54, }, { type: "STATE_SNAPSHOT", @@ -10006,7 +9365,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { invalid_tool_calls: [], type: "ai", }, - input: shared74, + input: shared65, }, run_id: "id-24", name: "ChatOpenAI", @@ -10041,7 +9400,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared68, + snapshot: shared59, rawEvent: { id: "18", event: "events", @@ -10144,7 +9503,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-22", name: "model_request", tags: ["graph:step:8"], - metadata: shared110, + metadata: shared101, }, }, }, @@ -10158,7 +9517,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared68, + snapshot: shared59, rawEvent: { id: "20", event: "events", @@ -10168,7 +9527,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_model", tags: ["graph:step:9"], run_id: "id-26", - metadata: shared96, + metadata: shared87, }, }, }, @@ -10271,7 +9630,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "RunnableLambda", tags: [], run_id: "id-28", - metadata: shared96, + metadata: shared87, }, }, }, @@ -10464,7 +9823,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-28", name: "RunnableLambda", tags: [], - metadata: shared96, + metadata: shared87, }, }, }, @@ -10567,7 +9926,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-26", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:9"], - metadata: shared96, + metadata: shared87, }, }, }, @@ -10678,7 +10037,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:10"], run_id: "id-29", - metadata: shared89, + metadata: shared80, }, }, }, @@ -10781,7 +10140,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-29", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:10"], - metadata: shared89, + metadata: shared80, }, }, }, @@ -11092,9 +10451,9 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { role: "user", content: "Hi change the background color to blue", }, - shared118, - shared122, - shared124, + shared109, + shared113, + shared115, ], }, { @@ -11204,7 +10563,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { content: "Hi change the background color to pink", }, ], - tools: shared112, + tools: shared103, context: [ { description: "Name of the user", @@ -11324,7 +10683,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:13"], run_id: "id-33", - metadata: shared76, + metadata: shared67, }, }, }, @@ -11625,7 +10984,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-33", name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:13"], - metadata: shared76, + metadata: shared67, }, }, }, @@ -11743,7 +11102,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "model_request", tags: ["graph:step:14"], run_id: "id-36", - metadata: shared105, + metadata: shared96, }, }, }, @@ -11986,109 +11345,6 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { data: shared49, }, }, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - id: "id-3", - content: "Hi change the background color to blue", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: "", - id: "id-10", - invalid_tool_calls: [], - additional_kwargs: {}, - response_metadata: {}, - tool_calls: [ - { - name: "change_background", - args: { - background: "blue", - }, - id: "id-11", - type: "tool_call", - }, - ], - type: "ai", - }, - { - content: - '{"status":"success","message":"Background changed to blue"}', - tool_call_id: "id-11", - id: "id-18", - status: "success", - additional_kwargs: {}, - response_metadata: {}, - type: "tool", - }, - { - content: "Done! I've completed that for you.", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - usage: { - prompt_tokens: 32, - completion_tokens: 9, - total_tokens: 41, - }, - }, - tool_call_chunks: [], - id: "id-25", - usage_metadata: { - input_tokens: 32, - output_tokens: 9, - total_tokens: 41, - input_token_details: {}, - output_token_details: {}, - }, - invalid_tool_calls: [], - name: "model", - tool_calls: [], - type: "ai", - }, - { - id: "id-32", - content: "Hi change the background color to pink", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], - }, - }, - rawEvent: { - id: "10", - event: "messages", - data: shared62, - }, - }, { type: "STATE_SNAPSHOT", snapshot: { @@ -12293,165 +11549,62 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { { index: 0, function: { - arguments: "}", - }, - }, - ], - }, - id: "id-39", - response_metadata: { - model_provider: "openai", - usage: {}, - }, - tool_calls: [], - invalid_tool_calls: [ - { - name: "", - args: "}", - error: "Malformed args.", - type: "invalid_tool_call", - }, - ], - type: "ai", - }, - }, - run_id: "id-38", - name: "ChatOpenAI", - tags: [], - metadata: { - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_plan: "developer", - langgraph_host: "self-hosted", - langgraph_api_url: "", - thread_id: "id-1", - run_id: "id-34", - langgraph_step: 14, - langgraph_node: "model_request", - langgraph_triggers: ["branch:to:model_request"], - langgraph_path: ["__pregel_pull", "model_request"], - langgraph_checkpoint_ns: "model_request:id-37", - checkpoint_ns: "model_request:id-37", - ls_provider: "openai", - ls_model_name: "gpt-4o", - ls_model_type: "chat", - versions: { - "@langchain/core": "1.1.46", - }, - }, - }, - }, - { - type: "TOOL_CALL_END", - toolCallId: "id-40", - rawEvent: shared49, - }, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - id: "id-3", - content: "Hi change the background color to blue", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: "", - id: "id-10", - invalid_tool_calls: [], - additional_kwargs: {}, - response_metadata: {}, - tool_calls: [ - { - name: "change_background", - args: { - background: "blue", - }, - id: "id-11", - type: "tool_call", - }, - ], - type: "ai", - }, - { - content: - '{"status":"success","message":"Background changed to blue"}', - tool_call_id: "id-11", - id: "id-18", - status: "success", - additional_kwargs: {}, - response_metadata: {}, - type: "tool", - }, - { - content: "Done! I've completed that for you.", - additional_kwargs: {}, + arguments: "}", + }, + }, + ], + }, + id: "id-39", response_metadata: { model_provider: "openai", - usage: { - prompt_tokens: 32, - completion_tokens: 9, - total_tokens: 41, - }, - }, - tool_call_chunks: [], - id: "id-25", - usage_metadata: { - input_tokens: 32, - output_tokens: 9, - total_tokens: 41, - input_token_details: {}, - output_token_details: {}, + usage: {}, }, - invalid_tool_calls: [], - name: "model", tool_calls: [], + invalid_tool_calls: [ + { + name: "", + args: "}", + error: "Malformed args.", + type: "invalid_tool_call", + }, + ], type: "ai", }, - { - id: "id-32", - content: "Hi change the background color to pink", - additional_kwargs: {}, - response_metadata: {}, - type: "human", + }, + run_id: "id-38", + name: "ChatOpenAI", + tags: [], + metadata: { + ls_integration: "langchain_chat_model", + created_by: "system", + graph_id: "agentic_chat", + assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", + run_attempt: 1, + langgraph_version: "", + langgraph_plan: "developer", + langgraph_host: "self-hosted", + langgraph_api_url: "", + thread_id: "id-1", + run_id: "id-34", + langgraph_step: 14, + langgraph_node: "model_request", + langgraph_triggers: ["branch:to:model_request"], + langgraph_path: ["__pregel_pull", "model_request"], + langgraph_checkpoint_ns: "model_request:id-37", + checkpoint_ns: "model_request:id-37", + ls_provider: "openai", + ls_model_name: "gpt-4o", + ls_model_type: "chat", + versions: { + "@langchain/core": "1.1.46", }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], }, }, - rawEvent: { - id: "18", - event: "messages", - data: shared62, - }, + }, + { + type: "TOOL_CALL_END", + toolCallId: "id-40", + rawEvent: shared49, }, { type: "STATE_SNAPSHOT", @@ -12808,7 +11961,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-36", name: "model_request", tags: ["graph:step:14"], - metadata: shared105, + metadata: shared96, }, }, }, @@ -12834,7 +11987,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_model", tags: ["graph:step:15"], run_id: "id-41", - metadata: shared84, + metadata: shared75, }, }, }, @@ -12850,7 +12003,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "RunnableLambda", tags: [], run_id: "id-43", - metadata: shared84, + metadata: shared75, }, }, }, @@ -12983,7 +12136,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-43", name: "RunnableLambda", tags: [], - metadata: shared84, + metadata: shared75, }, }, }, @@ -13230,7 +12383,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-41", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:15"], - metadata: shared84, + metadata: shared75, }, }, }, @@ -13368,7 +12521,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:16"], run_id: "id-44", - metadata: shared91, + metadata: shared82, }, }, }, @@ -13501,7 +12654,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-44", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:16"], - metadata: shared91, + metadata: shared82, }, }, }, @@ -13551,15 +12704,15 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { role: "user", content: "Hi change the background color to blue", }, - shared118, - shared122, - shared124, + shared109, + shared113, + shared115, { id: "id-32", role: "user", content: "Hi change the background color to pink", }, - shared119, + shared110, ], }, { @@ -13576,7 +12729,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { runId: "id-46", state: shared20, messages: [ - shared119, + shared110, { id: "id-47", content: @@ -13585,7 +12738,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { toolCallId: "id-40", }, ], - tools: shared112, + tools: shared103, context: [ { description: "Name of the user", @@ -13733,7 +12886,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:19"], run_id: "id-48", - metadata: shared78, + metadata: shared69, }, }, }, @@ -14100,174 +13253,36 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", parameters: { type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], - }, - }, - }, - run_id: "id-48", - name: "CopilotKitMiddleware.before_agent", - tags: ["graph:step:19"], - metadata: shared78, - }, - }, - }, - { - type: "STEP_FINISHED", - stepName: "CopilotKitMiddleware.before_agent", - }, - { - type: "STEP_STARTED", - stepName: "model_request", - }, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - id: "id-3", - content: "Hi change the background color to blue", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: "", - id: "id-10", - invalid_tool_calls: [], - additional_kwargs: {}, - response_metadata: {}, - tool_calls: [ - { - name: "change_background", - args: { - background: "blue", - }, - id: "id-11", - type: "tool_call", - }, - ], - type: "ai", - }, - { - content: - '{"status":"success","message":"Background changed to blue"}', - tool_call_id: "id-11", - id: "id-18", - status: "success", - additional_kwargs: {}, - response_metadata: {}, - type: "tool", - }, - { - content: "Done! I've completed that for you.", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - usage: { - prompt_tokens: 32, - completion_tokens: 9, - total_tokens: 41, - }, - }, - tool_call_chunks: [], - id: "id-25", - usage_metadata: { - input_tokens: 32, - output_tokens: 9, - total_tokens: 41, - input_token_details: {}, - output_token_details: {}, - }, - invalid_tool_calls: [], - name: "model", - tool_calls: [], - type: "ai", - }, - { - id: "id-32", - content: "Hi change the background color to pink", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: "", - id: "id-39", - invalid_tool_calls: [], - additional_kwargs: {}, - response_metadata: {}, - tool_calls: [ - { - name: "change_background", - args: { - background: "pink", - }, - id: "id-40", - type: "tool_call", - }, - ], - type: "ai", - }, - { - content: - '{"status":"success","message":"Background changed to pink"}', - tool_call_id: "id-40", - id: "id-47", - status: "success", - additional_kwargs: {}, - response_metadata: {}, - type: "tool", - }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", + properties: { + background: { + type: "string", + description: + "The background. Prefer gradients. Only use when asked.", + }, + }, + required: ["background"], + }, }, }, - required: ["background"], - }, + ], }, }, - ], - }, - }, - rawEvent: { - id: "6", - event: "events", - data: { - event: "on_chain_start", - data: shared17, - name: "model_request", - tags: ["graph:step:20"], - run_id: "id-51", - metadata: shared107, + }, + run_id: "id-48", + name: "CopilotKitMiddleware.before_agent", + tags: ["graph:step:19"], + metadata: shared69, }, }, }, + { + type: "STEP_FINISHED", + stepName: "CopilotKitMiddleware.before_agent", + }, + { + type: "STEP_STARTED", + stepName: "model_request", + }, { type: "STATE_SNAPSHOT", snapshot: { @@ -14394,41 +13409,15 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, }, rawEvent: { - id: "8", + id: "6", event: "events", data: { - event: "on_chat_model_start", - data: { - input: shared22, - }, - name: "ChatOpenAI", - tags: [], - run_id: "id-53", - metadata: { - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_plan: "developer", - langgraph_host: "self-hosted", - langgraph_api_url: "", - thread_id: "id-1", - run_id: "id-49", - langgraph_step: 20, - langgraph_node: "model_request", - langgraph_triggers: ["branch:to:model_request"], - langgraph_path: ["__pregel_pull", "model_request"], - langgraph_checkpoint_ns: "model_request:id-52", - checkpoint_ns: "model_request:id-52", - ls_provider: "openai", - ls_model_name: "gpt-4o", - ls_model_type: "chat", - versions: { - "@langchain/core": "1.1.46", - }, - }, + event: "on_chain_start", + data: shared17, + name: "model_request", + tags: ["graph:step:20"], + run_id: "id-51", + metadata: shared98, }, }, }, @@ -14558,9 +13547,42 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, }, rawEvent: { - id: "9", + id: "8", event: "events", - data: shared51, + data: { + event: "on_chat_model_start", + data: { + input: shared22, + }, + name: "ChatOpenAI", + tags: [], + run_id: "id-53", + metadata: { + ls_integration: "langchain_chat_model", + created_by: "system", + graph_id: "agentic_chat", + assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", + run_attempt: 1, + langgraph_version: "", + langgraph_plan: "developer", + langgraph_host: "self-hosted", + langgraph_api_url: "", + thread_id: "id-1", + run_id: "id-49", + langgraph_step: 20, + langgraph_node: "model_request", + langgraph_triggers: ["branch:to:model_request"], + langgraph_path: ["__pregel_pull", "model_request"], + langgraph_checkpoint_ns: "model_request:id-52", + checkpoint_ns: "model_request:id-52", + ls_provider: "openai", + ls_model_name: "gpt-4o", + ls_model_type: "chat", + versions: { + "@langchain/core": "1.1.46", + }, + }, + }, }, }, { @@ -14689,9 +13711,9 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, }, rawEvent: { - id: "10", - event: "messages", - data: shared64, + id: "9", + event: "events", + data: shared51, }, }, { @@ -15018,137 +14040,6 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { ], }, }, - rawEvent: { - id: "16", - event: "messages", - data: shared64, - }, - }, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - id: "id-3", - content: "Hi change the background color to blue", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: "", - id: "id-10", - invalid_tool_calls: [], - additional_kwargs: {}, - response_metadata: {}, - tool_calls: [ - { - name: "change_background", - args: { - background: "blue", - }, - id: "id-11", - type: "tool_call", - }, - ], - type: "ai", - }, - { - content: - '{"status":"success","message":"Background changed to blue"}', - tool_call_id: "id-11", - id: "id-18", - status: "success", - additional_kwargs: {}, - response_metadata: {}, - type: "tool", - }, - { - content: "Done! I've completed that for you.", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - usage: { - prompt_tokens: 32, - completion_tokens: 9, - total_tokens: 41, - }, - }, - tool_call_chunks: [], - id: "id-25", - usage_metadata: { - input_tokens: 32, - output_tokens: 9, - total_tokens: 41, - input_token_details: {}, - output_token_details: {}, - }, - invalid_tool_calls: [], - name: "model", - tool_calls: [], - type: "ai", - }, - { - id: "id-32", - content: "Hi change the background color to pink", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: "", - id: "id-39", - invalid_tool_calls: [], - additional_kwargs: {}, - response_metadata: {}, - tool_calls: [ - { - name: "change_background", - args: { - background: "pink", - }, - id: "id-40", - type: "tool_call", - }, - ], - type: "ai", - }, - { - content: - '{"status":"success","message":"Background changed to pink"}', - tool_call_id: "id-40", - id: "id-47", - status: "success", - additional_kwargs: {}, - response_metadata: {}, - type: "tool", - }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], - }, - }, rawEvent: { id: "17", event: "events", @@ -15214,7 +14105,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared69, + snapshot: shared60, rawEvent: { id: "18", event: "events", @@ -15377,7 +14268,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-51", name: "model_request", tags: ["graph:step:20"], - metadata: shared107, + metadata: shared98, }, }, }, @@ -15391,7 +14282,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared69, + snapshot: shared60, rawEvent: { id: "20", event: "events", @@ -15401,7 +14292,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_model", tags: ["graph:step:21"], run_id: "id-55", - metadata: shared86, + metadata: shared77, }, }, }, @@ -15564,7 +14455,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "RunnableLambda", tags: [], run_id: "id-57", - metadata: shared86, + metadata: shared77, }, }, }, @@ -15877,7 +14768,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-57", name: "RunnableLambda", tags: [], - metadata: shared86, + metadata: shared77, }, }, }, @@ -16040,7 +14931,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-55", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:21"], - metadata: shared86, + metadata: shared77, }, }, }, @@ -16211,7 +15102,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:22"], run_id: "id-58", - metadata: shared93, + metadata: shared84, }, }, }, @@ -16374,7 +15265,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-58", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:22"], - metadata: shared93, + metadata: shared84, }, }, }, @@ -16865,15 +15756,15 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { role: "user", content: "Hi change the background color to blue", }, - shared118, - shared122, - shared124, + shared109, + shared113, + shared115, { id: "id-32", role: "user", content: "Hi change the background color to pink", }, - shared119, + shared110, { id: "id-47", role: "tool", @@ -16911,7 +15802,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { content: "Hey there", }, ], - tools: shared112, + tools: shared103, context: [ { description: "Name of the user", @@ -16967,11 +15858,11 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { event: "events", data: { event: "on_chain_start", - data: shared103, + data: shared94, name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:1"], run_id: "id-4", - metadata: shared82, + metadata: shared73, }, }, }, @@ -17092,7 +15983,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-4", name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:1"], - metadata: shared82, + metadata: shared73, }, }, }, @@ -17146,11 +16037,11 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { event: "events", data: { event: "on_chain_start", - data: shared103, + data: shared94, name: "model_request", tags: ["graph:step:2"], run_id: "id-7", - metadata: shared111, + metadata: shared102, }, }, }, @@ -17197,7 +16088,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { data: { event: "on_chat_model_start", data: { - input: shared116, + input: shared107, }, name: "ChatOpenAI", tags: [], @@ -17269,45 +16160,6 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, rawEvent: shared36, }, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - id: "id-3", - content: "Hey there", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], - }, - }, - rawEvent: shared57, - }, { type: "STATE_SNAPSHOT", snapshot: { @@ -17415,45 +16267,6 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, }, shared34, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - id: "id-3", - content: "Hey there", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], - }, - }, - rawEvent: shared58, - }, { type: "STATE_SNAPSHOT", snapshot: { @@ -17521,7 +16334,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { invalid_tool_calls: [], type: "ai", }, - input: shared116, + input: shared107, }, run_id: "id-9", name: "ChatOpenAI", @@ -17556,7 +16369,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared70, + snapshot: shared61, rawEvent: { id: "18", event: "events", @@ -17631,7 +16444,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-7", name: "model_request", tags: ["graph:step:2"], - metadata: shared111, + metadata: shared102, }, }, }, @@ -17645,7 +16458,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared70, + snapshot: shared61, rawEvent: { id: "20", event: "events", @@ -17655,7 +16468,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_model", tags: ["graph:step:3"], run_id: "id-11", - metadata: shared97, + metadata: shared88, }, }, }, @@ -17730,7 +16543,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "RunnableLambda", tags: [], run_id: "id-13", - metadata: shared97, + metadata: shared88, }, }, }, @@ -17867,7 +16680,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-13", name: "RunnableLambda", tags: [], - metadata: shared97, + metadata: shared88, }, }, }, @@ -17942,7 +16755,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-11", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:3"], - metadata: shared97, + metadata: shared88, }, }, }, @@ -18025,7 +16838,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:4"], run_id: "id-14", - metadata: shared99, + metadata: shared90, }, }, }, @@ -18100,7 +16913,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-14", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:4"], - metadata: shared99, + metadata: shared90, }, }, }, @@ -18236,7 +17049,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-5", name: "LangGraph", tags: [], - metadata: shared113, + metadata: shared104, }, }, }, @@ -18315,7 +17128,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { role: "user", content: "Hey there", }, - shared123, + shared114, ], }, { @@ -18397,7 +17210,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { content: "My favorite fruit is Mango", }, ], - tools: shared112, + tools: shared103, context: [ { description: "Name of the user", @@ -18489,7 +17302,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:7"], run_id: "id-18", - metadata: shared80, + metadata: shared71, }, }, }, @@ -18706,7 +17519,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-18", name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:7"], - metadata: shared80, + metadata: shared71, }, }, }, @@ -18796,7 +17609,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "model_request", tags: ["graph:step:8"], run_id: "id-21", - metadata: shared109, + metadata: shared100, }, }, }, @@ -18875,7 +17688,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { data: { event: "on_chat_model_start", data: { - input: shared73, + input: shared64, }, name: "ChatOpenAI", tags: [], @@ -18983,81 +17796,6 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { data: shared53, }, }, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - id: "id-3", - content: "Hey there", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: "Hello! How can I assist you today?", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - usage: { - prompt_tokens: 10, - completion_tokens: 9, - total_tokens: 19, - }, - }, - tool_call_chunks: [], - id: "id-10", - usage_metadata: { - input_tokens: 10, - output_tokens: 9, - total_tokens: 19, - input_token_details: {}, - output_token_details: {}, - }, - invalid_tool_calls: [], - name: "model", - tool_calls: [], - type: "ai", - }, - { - id: "id-17", - content: "My favorite fruit is Mango", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], - }, - }, - rawEvent: { - id: "10", - event: "messages", - data: shared66, - }, - }, { type: "STATE_SNAPSHOT", snapshot: { @@ -19332,102 +18070,27 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { langgraph_plan: "developer", langgraph_host: "self-hosted", langgraph_api_url: "", - thread_id: "id-1", - run_id: "id-19", - langgraph_step: 8, - langgraph_node: "model_request", - langgraph_triggers: ["branch:to:model_request"], - langgraph_path: ["__pregel_pull", "model_request"], - langgraph_checkpoint_ns: "model_request:id-22", - checkpoint_ns: "model_request:id-22", - ls_provider: "openai", - ls_model_name: "gpt-4o", - ls_model_type: "chat", - versions: { - "@langchain/core": "1.1.46", - }, - }, - }, - }, - { - type: "TEXT_MESSAGE_END", - messageId: "id-24", - rawEvent: shared53, - }, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - id: "id-3", - content: "Hey there", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: "Hello! How can I assist you today?", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - usage: { - prompt_tokens: 10, - completion_tokens: 9, - total_tokens: 19, - }, - }, - tool_call_chunks: [], - id: "id-10", - usage_metadata: { - input_tokens: 10, - output_tokens: 9, - total_tokens: 19, - input_token_details: {}, - output_token_details: {}, - }, - invalid_tool_calls: [], - name: "model", - tool_calls: [], - type: "ai", - }, - { - id: "id-17", - content: "My favorite fruit is Mango", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], + thread_id: "id-1", + run_id: "id-19", + langgraph_step: 8, + langgraph_node: "model_request", + langgraph_triggers: ["branch:to:model_request"], + langgraph_path: ["__pregel_pull", "model_request"], + langgraph_checkpoint_ns: "model_request:id-22", + checkpoint_ns: "model_request:id-22", + ls_provider: "openai", + ls_model_name: "gpt-4o", + ls_model_type: "chat", + versions: { + "@langchain/core": "1.1.46", + }, }, }, - rawEvent: { - id: "22", - event: "messages", - data: shared66, - }, + }, + { + type: "TEXT_MESSAGE_END", + messageId: "id-24", + rawEvent: shared53, }, { type: "STATE_SNAPSHOT", @@ -19529,7 +18192,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { invalid_tool_calls: [], type: "ai", }, - input: shared73, + input: shared64, }, run_id: "id-23", name: "ChatOpenAI", @@ -19672,7 +18335,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-21", name: "model_request", tags: ["graph:step:8"], - metadata: shared109, + metadata: shared100, }, }, }, @@ -19696,7 +18359,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_model", tags: ["graph:step:9"], run_id: "id-25", - metadata: shared95, + metadata: shared86, }, }, }, @@ -19804,7 +18467,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "RunnableLambda", tags: [], run_id: "id-27", - metadata: shared95, + metadata: shared86, }, }, }, @@ -20007,7 +18670,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-27", name: "RunnableLambda", tags: [], - metadata: shared95, + metadata: shared86, }, }, }, @@ -20115,7 +18778,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-25", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:9"], - metadata: shared95, + metadata: shared86, }, }, }, @@ -20231,7 +18894,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:10"], run_id: "id-28", - metadata: shared88, + metadata: shared79, }, }, }, @@ -20339,7 +19002,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-28", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:10"], - metadata: shared88, + metadata: shared79, }, }, }, @@ -20665,13 +19328,13 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { role: "user", content: "Hey there", }, - shared123, + shared114, { id: "id-17", role: "user", content: "My favorite fruit is Mango", }, - shared121, + shared112, ], }, { @@ -20786,7 +19449,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { content: "and I love listening to Kaavish", }, ], - tools: shared112, + tools: shared103, context: [ { description: "Name of the user", @@ -20911,7 +19574,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:13"], run_id: "id-32", - metadata: shared75, + metadata: shared66, }, }, }, @@ -21227,7 +19890,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-32", name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:13"], - metadata: shared75, + metadata: shared66, }, }, }, @@ -21350,7 +20013,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "model_request", tags: ["graph:step:14"], run_id: "id-35", - metadata: shared104, + metadata: shared95, }, }, }, @@ -21705,114 +20368,6 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { ], }, }, - rawEvent: { - id: "10", - event: "messages", - data: shared61, - }, - }, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - id: "id-3", - content: "Hey there", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: "Hello! How can I assist you today?", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - usage: { - prompt_tokens: 10, - completion_tokens: 9, - total_tokens: 19, - }, - }, - tool_call_chunks: [], - id: "id-10", - usage_metadata: { - input_tokens: 10, - output_tokens: 9, - total_tokens: 19, - input_token_details: {}, - output_token_details: {}, - }, - invalid_tool_calls: [], - name: "model", - tool_calls: [], - type: "ai", - }, - { - id: "id-17", - content: "My favorite fruit is Mango", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: - "That's great! Mango is a wonderful tropical fruit known for its sweet, juicy flavor.", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - usage: { - prompt_tokens: 25, - completion_tokens: 21, - total_tokens: 46, - }, - }, - tool_call_chunks: [], - id: "id-24", - usage_metadata: { - input_tokens: 25, - output_tokens: 21, - total_tokens: 46, - input_token_details: {}, - output_token_details: {}, - }, - invalid_tool_calls: [], - name: "model", - tool_calls: [], - type: "ai", - }, - { - id: "id-31", - content: "and I love listening to Kaavish", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], - }, - }, rawEvent: { id: "11", event: "events", @@ -21991,162 +20546,54 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { rawEvent: { event: "on_chat_model_stream", data: { - chunk: { - content: " Western sounds!", - tool_call_chunks: [], - additional_kwargs: {}, - id: "id-38", - response_metadata: { - model_provider: "openai", - usage: {}, - }, - tool_calls: [], - invalid_tool_calls: [], - type: "ai", - }, - }, - run_id: "id-37", - name: "ChatOpenAI", - tags: [], - metadata: { - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_plan: "developer", - langgraph_host: "self-hosted", - langgraph_api_url: "", - thread_id: "id-1", - run_id: "id-33", - langgraph_step: 14, - langgraph_node: "model_request", - langgraph_triggers: ["branch:to:model_request"], - langgraph_path: ["__pregel_pull", "model_request"], - langgraph_checkpoint_ns: "model_request:id-36", - checkpoint_ns: "model_request:id-36", - ls_provider: "openai", - ls_model_name: "gpt-4o", - ls_model_type: "chat", - versions: { - "@langchain/core": "1.1.46", - }, - }, - }, - }, - { - type: "TEXT_MESSAGE_END", - messageId: "id-38", - rawEvent: shared48, - }, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - id: "id-3", - content: "Hey there", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: "Hello! How can I assist you today?", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - usage: { - prompt_tokens: 10, - completion_tokens: 9, - total_tokens: 19, - }, - }, - tool_call_chunks: [], - id: "id-10", - usage_metadata: { - input_tokens: 10, - output_tokens: 9, - total_tokens: 19, - input_token_details: {}, - output_token_details: {}, - }, - invalid_tool_calls: [], - name: "model", - tool_calls: [], - type: "ai", - }, - { - id: "id-17", - content: "My favorite fruit is Mango", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: - "That's great! Mango is a wonderful tropical fruit known for its sweet, juicy flavor.", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - usage: { - prompt_tokens: 25, - completion_tokens: 21, - total_tokens: 46, - }, - }, - tool_call_chunks: [], - id: "id-24", - usage_metadata: { - input_tokens: 25, - output_tokens: 21, - total_tokens: 46, - input_token_details: {}, - output_token_details: {}, + chunk: { + content: " Western sounds!", + tool_call_chunks: [], + additional_kwargs: {}, + id: "id-38", + response_metadata: { + model_provider: "openai", + usage: {}, }, - invalid_tool_calls: [], - name: "model", tool_calls: [], + invalid_tool_calls: [], type: "ai", }, - { - id: "id-31", - content: "and I love listening to Kaavish", - additional_kwargs: {}, - response_metadata: {}, - type: "human", + }, + run_id: "id-37", + name: "ChatOpenAI", + tags: [], + metadata: { + ls_integration: "langchain_chat_model", + created_by: "system", + graph_id: "agentic_chat", + assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", + run_attempt: 1, + langgraph_version: "", + langgraph_plan: "developer", + langgraph_host: "self-hosted", + langgraph_api_url: "", + thread_id: "id-1", + run_id: "id-33", + langgraph_step: 14, + langgraph_node: "model_request", + langgraph_triggers: ["branch:to:model_request"], + langgraph_path: ["__pregel_pull", "model_request"], + langgraph_checkpoint_ns: "model_request:id-36", + checkpoint_ns: "model_request:id-36", + ls_provider: "openai", + ls_model_name: "gpt-4o", + ls_model_type: "chat", + versions: { + "@langchain/core": "1.1.46", }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], }, }, - rawEvent: { - id: "22", - event: "messages", - data: shared61, - }, + }, + { + type: "TEXT_MESSAGE_END", + messageId: "id-38", + rawEvent: shared48, }, { type: "STATE_SNAPSHOT", @@ -22457,7 +20904,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-35", name: "model_request", tags: ["graph:step:14"], - metadata: shared104, + metadata: shared95, }, }, }, @@ -22481,7 +20928,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_model", tags: ["graph:step:15"], run_id: "id-39", - metadata: shared83, + metadata: shared74, }, }, }, @@ -22622,7 +21069,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "RunnableLambda", tags: [], run_id: "id-41", - metadata: shared83, + metadata: shared74, }, }, }, @@ -22891,7 +21338,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-41", name: "RunnableLambda", tags: [], - metadata: shared83, + metadata: shared74, }, }, }, @@ -23032,7 +21479,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-39", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:15"], - metadata: shared83, + metadata: shared74, }, }, }, @@ -23181,7 +21628,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:16"], run_id: "id-42", - metadata: shared90, + metadata: shared81, }, }, }, @@ -23322,7 +21769,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-42", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:16"], - metadata: shared90, + metadata: shared81, }, }, }, @@ -23747,19 +22194,19 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { role: "user", content: "Hey there", }, - shared123, + shared114, { id: "id-17", role: "user", content: "My favorite fruit is Mango", }, - shared121, + shared112, { id: "id-31", role: "user", content: "and I love listening to Kaavish", }, - shared120, + shared111, ], }, { @@ -23907,7 +22354,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { content: "tell me an interesting fact about Moon", }, ], - tools: shared112, + tools: shared103, context: [ { description: "Name of the user", @@ -24065,7 +22512,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:19"], run_id: "id-46", - metadata: shared77, + metadata: shared68, }, }, }, @@ -24447,199 +22894,51 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { id: "id-45", content: "tell me an interesting fact about Moon", additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], - }, - }, - }, - run_id: "id-46", - name: "CopilotKitMiddleware.before_agent", - tags: ["graph:step:19"], - metadata: shared77, - }, - }, - }, - { - type: "STEP_FINISHED", - stepName: "CopilotKitMiddleware.before_agent", - }, - { - type: "STEP_STARTED", - stepName: "model_request", - }, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - id: "id-3", - content: "Hey there", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: "Hello! How can I assist you today?", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - usage: { - prompt_tokens: 10, - completion_tokens: 9, - total_tokens: 19, - }, - }, - tool_call_chunks: [], - id: "id-10", - usage_metadata: { - input_tokens: 10, - output_tokens: 9, - total_tokens: 19, - input_token_details: {}, - output_token_details: {}, - }, - invalid_tool_calls: [], - name: "model", - tool_calls: [], - type: "ai", - }, - { - id: "id-17", - content: "My favorite fruit is Mango", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: - "That's great! Mango is a wonderful tropical fruit known for its sweet, juicy flavor.", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - usage: { - prompt_tokens: 25, - completion_tokens: 21, - total_tokens: 46, - }, - }, - tool_call_chunks: [], - id: "id-24", - usage_metadata: { - input_tokens: 25, - output_tokens: 21, - total_tokens: 46, - input_token_details: {}, - output_token_details: {}, - }, - invalid_tool_calls: [], - name: "model", - tool_calls: [], - type: "ai", - }, - { - id: "id-31", - content: "and I love listening to Kaavish", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: - "Kaavish is a wonderful musical group known for their unique blend of Eastern and Western sounds!", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - usage: { - prompt_tokens: 53, - completion_tokens: 24, - total_tokens: 77, - }, - }, - tool_call_chunks: [], - id: "id-38", - usage_metadata: { - input_tokens: 53, - output_tokens: 24, - total_tokens: 77, - input_token_details: {}, - output_token_details: {}, - }, - invalid_tool_calls: [], - name: "model", - tool_calls: [], - type: "ai", - }, - { - id: "id-45", - content: "tell me an interesting fact about Moon", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", + response_metadata: {}, + type: "human", + }, + ], + copilotkit: { + actions: [ + { + type: "function", + name: "change_background", + function: { + name: "change_background", description: - "The background. Prefer gradients. Only use when asked.", + "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", + parameters: { + type: "object", + properties: { + background: { + type: "string", + description: + "The background. Prefer gradients. Only use when asked.", + }, + }, + required: ["background"], + }, }, }, - required: ["background"], - }, + ], }, }, - ], - }, - }, - rawEvent: { - id: "6", - event: "events", - data: { - event: "on_chain_start", - data: shared12, - name: "model_request", - tags: ["graph:step:20"], - run_id: "id-49", - metadata: shared106, + }, + run_id: "id-46", + name: "CopilotKitMiddleware.before_agent", + tags: ["graph:step:19"], + metadata: shared68, }, }, }, + { + type: "STEP_FINISHED", + stepName: "CopilotKitMiddleware.before_agent", + }, + { + type: "STEP_STARTED", + stepName: "model_request", + }, { type: "STATE_SNAPSHOT", snapshot: { @@ -24776,41 +23075,15 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, }, rawEvent: { - id: "8", + id: "6", event: "events", data: { - event: "on_chat_model_start", - data: { - input: shared18, - }, - name: "ChatOpenAI", - tags: [], - run_id: "id-51", - metadata: { - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_plan: "developer", - langgraph_host: "self-hosted", - langgraph_api_url: "", - thread_id: "id-1", - run_id: "id-47", - langgraph_step: 20, - langgraph_node: "model_request", - langgraph_triggers: ["branch:to:model_request"], - langgraph_path: ["__pregel_pull", "model_request"], - langgraph_checkpoint_ns: "model_request:id-50", - checkpoint_ns: "model_request:id-50", - ls_provider: "openai", - ls_model_name: "gpt-4o", - ls_model_type: "chat", - versions: { - "@langchain/core": "1.1.46", - }, - }, + event: "on_chain_start", + data: shared12, + name: "model_request", + tags: ["graph:step:20"], + run_id: "id-49", + metadata: shared97, }, }, }, @@ -24950,9 +23223,42 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, }, rawEvent: { - id: "9", + id: "8", event: "events", - data: shared50, + data: { + event: "on_chat_model_start", + data: { + input: shared18, + }, + name: "ChatOpenAI", + tags: [], + run_id: "id-51", + metadata: { + ls_integration: "langchain_chat_model", + created_by: "system", + graph_id: "agentic_chat", + assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", + run_attempt: 1, + langgraph_version: "", + langgraph_plan: "developer", + langgraph_host: "self-hosted", + langgraph_api_url: "", + thread_id: "id-1", + run_id: "id-47", + langgraph_step: 20, + langgraph_node: "model_request", + langgraph_triggers: ["branch:to:model_request"], + langgraph_path: ["__pregel_pull", "model_request"], + langgraph_checkpoint_ns: "model_request:id-50", + checkpoint_ns: "model_request:id-50", + ls_provider: "openai", + ls_model_name: "gpt-4o", + ls_model_type: "chat", + versions: { + "@langchain/core": "1.1.46", + }, + }, + }, }, }, { @@ -25091,9 +23397,9 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, }, rawEvent: { - id: "10", - event: "messages", - data: shared63, + id: "9", + event: "events", + data: shared50, }, }, { @@ -25594,163 +23900,22 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { langgraph_step: 20, langgraph_node: "model_request", langgraph_triggers: ["branch:to:model_request"], - langgraph_path: ["__pregel_pull", "model_request"], - langgraph_checkpoint_ns: "model_request:id-50", - checkpoint_ns: "model_request:id-50", - ls_provider: "openai", - ls_model_name: "gpt-4o", - ls_model_type: "chat", - versions: { - "@langchain/core": "1.1.46", - }, - }, - }, - }, - { - type: "TEXT_MESSAGE_END", - messageId: "id-52", - rawEvent: shared50, - }, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - id: "id-3", - content: "Hey there", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: "Hello! How can I assist you today?", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - usage: { - prompt_tokens: 10, - completion_tokens: 9, - total_tokens: 19, - }, - }, - tool_call_chunks: [], - id: "id-10", - usage_metadata: { - input_tokens: 10, - output_tokens: 9, - total_tokens: 19, - input_token_details: {}, - output_token_details: {}, - }, - invalid_tool_calls: [], - name: "model", - tool_calls: [], - type: "ai", - }, - { - id: "id-17", - content: "My favorite fruit is Mango", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: - "That's great! Mango is a wonderful tropical fruit known for its sweet, juicy flavor.", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - usage: { - prompt_tokens: 25, - completion_tokens: 21, - total_tokens: 46, - }, - }, - tool_call_chunks: [], - id: "id-24", - usage_metadata: { - input_tokens: 25, - output_tokens: 21, - total_tokens: 46, - input_token_details: {}, - output_token_details: {}, - }, - invalid_tool_calls: [], - name: "model", - tool_calls: [], - type: "ai", - }, - { - id: "id-31", - content: "and I love listening to Kaavish", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: - "Kaavish is a wonderful musical group known for their unique blend of Eastern and Western sounds!", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - usage: { - prompt_tokens: 53, - completion_tokens: 24, - total_tokens: 77, - }, - }, - tool_call_chunks: [], - id: "id-38", - usage_metadata: { - input_tokens: 53, - output_tokens: 24, - total_tokens: 77, - input_token_details: {}, - output_token_details: {}, - }, - invalid_tool_calls: [], - name: "model", - tool_calls: [], - type: "ai", - }, - { - id: "id-45", - content: "tell me an interesting fact about Moon", - additional_kwargs: {}, - response_metadata: {}, - type: "human", + langgraph_path: ["__pregel_pull", "model_request"], + langgraph_checkpoint_ns: "model_request:id-50", + checkpoint_ns: "model_request:id-50", + ls_provider: "openai", + ls_model_name: "gpt-4o", + ls_model_type: "chat", + versions: { + "@langchain/core": "1.1.46", }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], }, }, - rawEvent: { - id: "28", - event: "messages", - data: shared63, - }, + }, + { + type: "TEXT_MESSAGE_END", + messageId: "id-52", + rawEvent: shared50, }, { type: "STATE_SNAPSHOT", @@ -26127,7 +24292,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-49", name: "model_request", tags: ["graph:step:20"], - metadata: shared106, + metadata: shared97, }, }, }, @@ -26151,7 +24316,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_model", tags: ["graph:step:21"], run_id: "id-53", - metadata: shared85, + metadata: shared76, }, }, }, @@ -26325,7 +24490,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "RunnableLambda", tags: [], run_id: "id-55", - metadata: shared85, + metadata: shared76, }, }, }, @@ -26660,7 +24825,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-55", name: "RunnableLambda", tags: [], - metadata: shared85, + metadata: shared76, }, }, }, @@ -26834,7 +24999,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-53", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:21"], - metadata: shared85, + metadata: shared76, }, }, }, @@ -27016,7 +25181,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:22"], run_id: "id-56", - metadata: shared92, + metadata: shared83, }, }, }, @@ -27190,7 +25355,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-56", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:22"], - metadata: shared92, + metadata: shared83, }, }, }, @@ -27714,25 +25879,25 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { role: "user", content: "Hey there", }, - shared123, + shared114, { id: "id-17", role: "user", content: "My favorite fruit is Mango", }, - shared121, + shared112, { id: "id-31", role: "user", content: "and I love listening to Kaavish", }, - shared120, + shared111, { id: "id-45", role: "user", content: "tell me an interesting fact about Moon", }, - shared117, + shared108, ], }, { @@ -27906,8 +26071,8 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { ], }, }, - messages: [shared125], - tools: shared112, + messages: [shared116], + tools: shared103, context: [ { description: "Name of the user", @@ -28098,7 +26263,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:25"], run_id: "id-60", - metadata: shared79, + metadata: shared70, }, }, }, @@ -28597,214 +26762,33 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { properties: { background: { type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], - }, - }, - }, - run_id: "id-60", - name: "CopilotKitMiddleware.before_agent", - tags: ["graph:step:25"], - metadata: shared79, - }, - }, - }, - { - type: "STEP_FINISHED", - stepName: "CopilotKitMiddleware.before_agent", - }, - { - type: "STEP_STARTED", - stepName: "model_request", - }, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - id: "id-3", - content: "Hey there", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: "Hello! How can I assist you today?", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - usage: { - prompt_tokens: 10, - completion_tokens: 9, - total_tokens: 19, - }, - }, - tool_call_chunks: [], - id: "id-10", - usage_metadata: { - input_tokens: 10, - output_tokens: 9, - total_tokens: 19, - input_token_details: {}, - output_token_details: {}, - }, - invalid_tool_calls: [], - name: "model", - tool_calls: [], - type: "ai", - }, - { - id: "id-17", - content: "My favorite fruit is Mango", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: - "That's great! Mango is a wonderful tropical fruit known for its sweet, juicy flavor.", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - usage: { - prompt_tokens: 25, - completion_tokens: 21, - total_tokens: 46, - }, - }, - tool_call_chunks: [], - id: "id-24", - usage_metadata: { - input_tokens: 25, - output_tokens: 21, - total_tokens: 46, - input_token_details: {}, - output_token_details: {}, - }, - invalid_tool_calls: [], - name: "model", - tool_calls: [], - type: "ai", - }, - { - id: "id-31", - content: "and I love listening to Kaavish", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: - "Kaavish is a wonderful musical group known for their unique blend of Eastern and Western sounds!", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - usage: { - prompt_tokens: 53, - completion_tokens: 24, - total_tokens: 77, - }, - }, - tool_call_chunks: [], - id: "id-38", - usage_metadata: { - input_tokens: 53, - output_tokens: 24, - total_tokens: 77, - input_token_details: {}, - output_token_details: {}, - }, - invalid_tool_calls: [], - name: "model", - tool_calls: [], - type: "ai", - }, - { - id: "id-45", - content: "tell me an interesting fact about Moon", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: - "The Moon is Earth's only natural satellite, orbiting at an average distance of about 384,400 km. It takes approximately 27.3 days to complete one orbit.", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - usage: { - prompt_tokens: 87, - completion_tokens: 38, - total_tokens: 125, - }, - }, - tool_call_chunks: [], - id: "id-52", - usage_metadata: { - input_tokens: 87, - output_tokens: 38, - total_tokens: 125, - input_token_details: {}, - output_token_details: {}, - }, - invalid_tool_calls: [], - name: "model", - tool_calls: [], - type: "ai", - }, - { - id: "id-59", - content: "Can you remind me what my favorite fruit is?", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], - }, - }, - rawEvent: { - id: "6", - event: "events", - data: { - event: "on_chain_start", - data: shared7, - name: "model_request", - tags: ["graph:step:26"], - run_id: "id-63", - metadata: shared108, + description: + "The background. Prefer gradients. Only use when asked.", + }, + }, + required: ["background"], + }, + }, + }, + ], + }, + }, + }, + run_id: "id-60", + name: "CopilotKitMiddleware.before_agent", + tags: ["graph:step:25"], + metadata: shared70, }, }, }, + { + type: "STEP_FINISHED", + stepName: "CopilotKitMiddleware.before_agent", + }, + { + type: "STEP_STARTED", + stepName: "model_request", + }, { type: "STATE_SNAPSHOT", snapshot: { @@ -28974,41 +26958,15 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, }, rawEvent: { - id: "8", + id: "6", event: "events", data: { - event: "on_chat_model_start", - data: { - input: shared10, - }, - name: "ChatOpenAI", - tags: [], - run_id: "id-65", - metadata: { - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_plan: "developer", - langgraph_host: "self-hosted", - langgraph_api_url: "", - thread_id: "id-1", - run_id: "id-61", - langgraph_step: 26, - langgraph_node: "model_request", - langgraph_triggers: ["branch:to:model_request"], - langgraph_path: ["__pregel_pull", "model_request"], - langgraph_checkpoint_ns: "model_request:id-64", - checkpoint_ns: "model_request:id-64", - ls_provider: "openai", - ls_model_name: "gpt-4o", - ls_model_type: "chat", - versions: { - "@langchain/core": "1.1.46", - }, - }, + event: "on_chain_start", + data: shared7, + name: "model_request", + tags: ["graph:step:26"], + run_id: "id-63", + metadata: shared99, }, }, }, @@ -29181,184 +27139,43 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, }, rawEvent: { - id: "9", + id: "8", event: "events", - data: shared52, - }, - }, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - id: "id-3", - content: "Hey there", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: "Hello! How can I assist you today?", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - usage: { - prompt_tokens: 10, - completion_tokens: 9, - total_tokens: 19, - }, - }, - tool_call_chunks: [], - id: "id-10", - usage_metadata: { - input_tokens: 10, - output_tokens: 9, - total_tokens: 19, - input_token_details: {}, - output_token_details: {}, - }, - invalid_tool_calls: [], - name: "model", - tool_calls: [], - type: "ai", - }, - { - id: "id-17", - content: "My favorite fruit is Mango", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: - "That's great! Mango is a wonderful tropical fruit known for its sweet, juicy flavor.", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - usage: { - prompt_tokens: 25, - completion_tokens: 21, - total_tokens: 46, - }, - }, - tool_call_chunks: [], - id: "id-24", - usage_metadata: { - input_tokens: 25, - output_tokens: 21, - total_tokens: 46, - input_token_details: {}, - output_token_details: {}, - }, - invalid_tool_calls: [], - name: "model", - tool_calls: [], - type: "ai", - }, - { - id: "id-31", - content: "and I love listening to Kaavish", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: - "Kaavish is a wonderful musical group known for their unique blend of Eastern and Western sounds!", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - usage: { - prompt_tokens: 53, - completion_tokens: 24, - total_tokens: 77, - }, - }, - tool_call_chunks: [], - id: "id-38", - usage_metadata: { - input_tokens: 53, - output_tokens: 24, - total_tokens: 77, - input_token_details: {}, - output_token_details: {}, - }, - invalid_tool_calls: [], - name: "model", - tool_calls: [], - type: "ai", - }, - { - id: "id-45", - content: "tell me an interesting fact about Moon", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - { - content: - "The Moon is Earth's only natural satellite, orbiting at an average distance of about 384,400 km. It takes approximately 27.3 days to complete one orbit.", - additional_kwargs: {}, - response_metadata: { - model_provider: "openai", - usage: { - prompt_tokens: 87, - completion_tokens: 38, - total_tokens: 125, - }, - }, - tool_call_chunks: [], - id: "id-52", - usage_metadata: { - input_tokens: 87, - output_tokens: 38, - total_tokens: 125, - input_token_details: {}, - output_token_details: {}, - }, - invalid_tool_calls: [], - name: "model", - tool_calls: [], - type: "ai", - }, - { - id: "id-59", - content: "Can you remind me what my favorite fruit is?", - additional_kwargs: {}, - response_metadata: {}, - type: "human", + data: { + event: "on_chat_model_start", + data: { + input: shared10, }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, + name: "ChatOpenAI", + tags: [], + run_id: "id-65", + metadata: { + ls_integration: "langchain_chat_model", + created_by: "system", + graph_id: "agentic_chat", + assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", + run_attempt: 1, + langgraph_version: "", + langgraph_plan: "developer", + langgraph_host: "self-hosted", + langgraph_api_url: "", + thread_id: "id-1", + run_id: "id-61", + langgraph_step: 26, + langgraph_node: "model_request", + langgraph_triggers: ["branch:to:model_request"], + langgraph_path: ["__pregel_pull", "model_request"], + langgraph_checkpoint_ns: "model_request:id-64", + checkpoint_ns: "model_request:id-64", + ls_provider: "openai", + ls_model_name: "gpt-4o", + ls_model_type: "chat", + versions: { + "@langchain/core": "1.1.46", }, - ], + }, }, }, - rawEvent: { - id: "10", - event: "messages", - data: shared65, - }, }, { type: "STATE_SNAPSHOT", @@ -29529,79 +27346,11 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, }, rawEvent: { - id: "11", + id: "9", event: "events", - data: shared41, - }, - }, - { - type: "TEXT_MESSAGE_START", - role: "assistant", - messageId: "id-66", - rawEvent: shared41, - }, - { - type: "TEXT_MESSAGE_CONTENT", - messageId: "id-66", - delta: "Your favorite fruit ", - rawEvent: shared41, - }, - { - type: "TEXT_MESSAGE_CONTENT", - messageId: "id-66", - delta: "is Mango!", - rawEvent: { - event: "on_chat_model_stream", - data: { - chunk: { - content: "is Mango!", - tool_call_chunks: [], - additional_kwargs: {}, - id: "id-66", - response_metadata: { - model_provider: "openai", - usage: {}, - }, - tool_calls: [], - invalid_tool_calls: [], - type: "ai", - }, - }, - run_id: "id-65", - name: "ChatOpenAI", - tags: [], - metadata: { - ls_integration: "langchain_chat_model", - created_by: "system", - graph_id: "agentic_chat", - assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", - run_attempt: 1, - langgraph_version: "", - langgraph_plan: "developer", - langgraph_host: "self-hosted", - langgraph_api_url: "", - thread_id: "id-1", - run_id: "id-61", - langgraph_step: 26, - langgraph_node: "model_request", - langgraph_triggers: ["branch:to:model_request"], - langgraph_path: ["__pregel_pull", "model_request"], - langgraph_checkpoint_ns: "model_request:id-64", - checkpoint_ns: "model_request:id-64", - ls_provider: "openai", - ls_model_name: "gpt-4o", - ls_model_type: "chat", - versions: { - "@langchain/core": "1.1.46", - }, - }, + data: shared52, }, }, - { - type: "TEXT_MESSAGE_END", - messageId: "id-66", - rawEvent: shared52, - }, { type: "STATE_SNAPSHOT", snapshot: { @@ -29771,11 +27520,79 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, }, rawEvent: { - id: "16", - event: "messages", - data: shared65, + id: "11", + event: "events", + data: shared41, + }, + }, + { + type: "TEXT_MESSAGE_START", + role: "assistant", + messageId: "id-66", + rawEvent: shared41, + }, + { + type: "TEXT_MESSAGE_CONTENT", + messageId: "id-66", + delta: "Your favorite fruit ", + rawEvent: shared41, + }, + { + type: "TEXT_MESSAGE_CONTENT", + messageId: "id-66", + delta: "is Mango!", + rawEvent: { + event: "on_chat_model_stream", + data: { + chunk: { + content: "is Mango!", + tool_call_chunks: [], + additional_kwargs: {}, + id: "id-66", + response_metadata: { + model_provider: "openai", + usage: {}, + }, + tool_calls: [], + invalid_tool_calls: [], + type: "ai", + }, + }, + run_id: "id-65", + name: "ChatOpenAI", + tags: [], + metadata: { + ls_integration: "langchain_chat_model", + created_by: "system", + graph_id: "agentic_chat", + assistant_id: "af77e4cd-2160-599e-9a20-86b1cdc1b3c7", + run_attempt: 1, + langgraph_version: "", + langgraph_plan: "developer", + langgraph_host: "self-hosted", + langgraph_api_url: "", + thread_id: "id-1", + run_id: "id-61", + langgraph_step: 26, + langgraph_node: "model_request", + langgraph_triggers: ["branch:to:model_request"], + langgraph_path: ["__pregel_pull", "model_request"], + langgraph_checkpoint_ns: "model_request:id-64", + checkpoint_ns: "model_request:id-64", + ls_provider: "openai", + ls_model_name: "gpt-4o", + ls_model_type: "chat", + versions: { + "@langchain/core": "1.1.46", + }, + }, }, }, + { + type: "TEXT_MESSAGE_END", + messageId: "id-66", + rawEvent: shared52, + }, { type: "STATE_SNAPSHOT", snapshot: { @@ -30009,7 +27826,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared71, + snapshot: shared62, rawEvent: { id: "18", event: "events", @@ -30215,7 +28032,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-63", name: "model_request", tags: ["graph:step:26"], - metadata: shared108, + metadata: shared99, }, }, }, @@ -30229,7 +28046,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared71, + snapshot: shared62, rawEvent: { id: "20", event: "events", @@ -30239,7 +28056,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_model", tags: ["graph:step:27"], run_id: "id-67", - metadata: shared87, + metadata: shared78, }, }, }, @@ -30445,7 +28262,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "RunnableLambda", tags: [], run_id: "id-69", - metadata: shared87, + metadata: shared78, }, }, }, @@ -30844,7 +28661,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-69", name: "RunnableLambda", tags: [], - metadata: shared87, + metadata: shared78, }, }, }, @@ -31050,7 +28867,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-67", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:27"], - metadata: shared87, + metadata: shared78, }, }, }, @@ -31264,7 +29081,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:28"], run_id: "id-70", - metadata: shared94, + metadata: shared85, }, }, }, @@ -31470,7 +29287,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-70", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:28"], - metadata: shared94, + metadata: shared85, }, }, }, @@ -32090,26 +29907,26 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { role: "user", content: "Hey there", }, - shared123, + shared114, { id: "id-17", role: "user", content: "My favorite fruit is Mango", }, - shared121, + shared112, { id: "id-31", role: "user", content: "and I love listening to Kaavish", }, - shared120, + shared111, { id: "id-45", role: "user", content: "tell me an interesting fact about Moon", }, - shared117, - shared125, + shared108, + shared116, { id: "id-66", role: "assistant", @@ -32140,7 +29957,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { content: "Hi, I am duaa", }, ], - tools: shared112, + tools: shared103, context: [ { description: "Name of the user", @@ -32196,11 +30013,11 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { event: "events", data: { event: "on_chain_start", - data: shared102, + data: shared93, name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:1"], run_id: "id-4", - metadata: shared82, + metadata: shared73, }, }, }, @@ -32321,7 +30138,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-4", name: "CopilotKitMiddleware.before_agent", tags: ["graph:step:1"], - metadata: shared82, + metadata: shared73, }, }, }, @@ -32375,11 +30192,11 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { event: "events", data: { event: "on_chain_start", - data: shared102, + data: shared93, name: "model_request", tags: ["graph:step:2"], run_id: "id-7", - metadata: shared111, + metadata: shared102, }, }, }, @@ -32426,7 +30243,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { data: { event: "on_chat_model_start", data: { - input: shared115, + input: shared106, }, name: "ChatOpenAI", tags: [], @@ -32498,45 +30315,6 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, rawEvent: shared36, }, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - id: "id-3", - content: "Hi, I am duaa", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], - }, - }, - rawEvent: shared57, - }, { type: "STATE_SNAPSHOT", snapshot: { @@ -32644,45 +30422,6 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, }, shared34, - { - type: "STATE_SNAPSHOT", - snapshot: { - messages: [ - { - id: "id-3", - content: "Hi, I am duaa", - additional_kwargs: {}, - response_metadata: {}, - type: "human", - }, - ], - copilotkit: { - actions: [ - { - type: "function", - name: "change_background", - function: { - name: "change_background", - description: - "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", - parameters: { - type: "object", - properties: { - background: { - type: "string", - description: - "The background. Prefer gradients. Only use when asked.", - }, - }, - required: ["background"], - }, - }, - }, - ], - }, - }, - rawEvent: shared58, - }, { type: "STATE_SNAPSHOT", snapshot: { @@ -32750,7 +30489,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { invalid_tool_calls: [], type: "ai", }, - input: shared115, + input: shared106, }, run_id: "id-9", name: "ChatOpenAI", @@ -32785,7 +30524,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared60, + snapshot: shared58, rawEvent: { id: "18", event: "events", @@ -32860,7 +30599,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-7", name: "model_request", tags: ["graph:step:2"], - metadata: shared111, + metadata: shared102, }, }, }, @@ -32874,7 +30613,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { }, { type: "STATE_SNAPSHOT", - snapshot: shared60, + snapshot: shared58, rawEvent: { id: "20", event: "events", @@ -32884,7 +30623,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_model", tags: ["graph:step:3"], run_id: "id-11", - metadata: shared97, + metadata: shared88, }, }, }, @@ -32959,7 +30698,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "RunnableLambda", tags: [], run_id: "id-13", - metadata: shared97, + metadata: shared88, }, }, }, @@ -33096,7 +30835,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-13", name: "RunnableLambda", tags: [], - metadata: shared97, + metadata: shared88, }, }, }, @@ -33171,7 +30910,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-11", name: "CopilotKitMiddleware.after_model", tags: ["graph:step:3"], - metadata: shared97, + metadata: shared88, }, }, }, @@ -33254,7 +30993,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:4"], run_id: "id-14", - metadata: shared99, + metadata: shared90, }, }, }, @@ -33329,7 +31068,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-14", name: "CopilotKitMiddleware.after_agent", tags: ["graph:step:4"], - metadata: shared99, + metadata: shared90, }, }, }, @@ -33465,7 +31204,7 @@ export const agenticChatPageEventTrace = defineEventTrace(import.meta.url, { run_id: "id-5", name: "LangGraph", tags: [], - metadata: shared113, + metadata: shared104, }, }, }, diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/shared_state/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/shared_state/page.tsx index 9b6c95caa2..bad2e6ca82 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/shared_state/page.tsx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/shared_state/page.tsx @@ -26,8 +26,14 @@ export default function SharedState({ params }: SharedStateProps) { const { isMobile } = useMobileView(); const { chatDefaultOpen } = useURLParams(); const defaultChatHeight = 50; - const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } = - useMobileChat(defaultChatHeight); + const { + isChatOpen, + setChatHeight, + setIsChatOpen, + isDragging, + chatHeight, + handleDragStart, + } = useMobileChat(defaultChatHeight); const chatTitle = "AI Recipe Assistant"; const chatDescription = "Ask me to craft recipes"; @@ -57,7 +63,9 @@ export default function SharedState({ params }: SharedStateProps) {
{chatTitle}
-
{chatDescription}
+
+ {chatDescription} +
setIsChatOpen(false)} /> +
setIsChatOpen(false)} + /> )} ) : ( @@ -247,6 +258,7 @@ function Recipe() { const agentState = agent.state as RecipeAgentState | undefined; const setAgentState = (s: RecipeAgentState) => agent.setState(s); const isLoading = agent.isRunning; + const isAgentReady = Boolean(copilotkit.getAgent("shared_state")); // Set initial state on mount useEffect(() => { @@ -260,7 +272,9 @@ function Recipe() { }, []); const [recipe, setRecipe] = useState(INITIAL_STATE.recipe); - const [editingInstructionIndex, setEditingInstructionIndex] = useState(null); + const [editingInstructionIndex, setEditingInstructionIndex] = useState< + number | null + >(null); const newInstructionRef = useRef(null); const updateRecipe = (partialRecipe: Partial) => { @@ -296,10 +310,13 @@ function Recipe() { if ( agentState && agentState.recipe && - (agentState.recipe as unknown as Record)[key] !== undefined && + (agentState.recipe as unknown as Record)[key] !== + undefined && (agentState.recipe as unknown as Record)[key] !== null ) { - let agentValue = (agentState.recipe as unknown as Record)[key]; + let agentValue = ( + agentState.recipe as unknown as Record + )[key]; const recipeValue = (recipe as unknown as Record)[key]; // Check if agentValue is a string and replace \n with actual newlines @@ -308,7 +325,8 @@ function Recipe() { } if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) { - (newRecipeState as unknown as Record)[key] = agentValue; + (newRecipeState as unknown as Record)[key] = + agentValue; newChangedKeys.push(key); } } @@ -335,7 +353,9 @@ function Recipe() { }); }; - const handleSkillLevelChange = (event: React.ChangeEvent) => { + const handleSkillLevelChange = ( + event: React.ChangeEvent, + ) => { updateRecipe({ skill_level: event.target.value as SkillLevel, }); @@ -348,12 +368,16 @@ function Recipe() { }); } else { updateRecipe({ - special_preferences: recipe.special_preferences.filter((p) => p !== preference), + special_preferences: recipe.special_preferences.filter( + (p) => p !== preference, + ), }); } }; - const handleCookingTimeChange = (event: React.ChangeEvent) => { + const handleCookingTimeChange = ( + event: React.ChangeEvent, + ) => { updateRecipe({ cooking_time: cookingTimeValues[Number(event.target.value)].label, }); @@ -362,11 +386,18 @@ function Recipe() { const addIngredient = () => { // Pick a random food emoji from our valid list updateRecipe({ - ingredients: [...recipe.ingredients, { icon: "🍴", name: "", amount: "" }], + ingredients: [ + ...recipe.ingredients, + { icon: "🍴", name: "", amount: "" }, + ], }); }; - const updateIngredient = (index: number, field: keyof Ingredient, value: string) => { + const updateIngredient = ( + index: number, + field: keyof Ingredient, + value: string, + ) => { const updatedIngredients = [...recipe.ingredients]; updatedIngredients[index] = { ...updatedIngredients[index], @@ -391,8 +422,12 @@ function Recipe() { // Focus the new instruction after render setTimeout(() => { - const textareas = document.querySelectorAll(".instructions-container textarea"); - const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement; + const textareas = document.querySelectorAll( + ".instructions-container textarea", + ); + const newTextarea = textareas[ + textareas.length - 1 + ] as HTMLTextAreaElement; if (newTextarea) { newTextarea.focus(); } @@ -424,6 +459,7 @@ function Recipe() { return (
@@ -441,7 +477,10 @@ function Recipe() { 🕒 updateIngredient(index, "name", e.target.value)} + onChange={(e) => + updateIngredient(index, "name", e.target.value) + } placeholder="Ingredient name" className="ingredient-name-input" /> updateIngredient(index, "amount", e.target.value)} + onChange={(e) => + updateIngredient(index, "amount", e.target.value) + } placeholder="Amount" className="ingredient-amount-input" /> @@ -559,18 +611,27 @@ function Recipe() { {changedKeysRef.current.includes("instructions") && }

Instructions

-
-
+
{recipe.instructions.map((instruction, index) => (
{/* Number Circle */}
{index + 1}
{/* Vertical Line */} - {index < recipe.instructions.length - 1 &&
} + {index < recipe.instructions.length - 1 && ( +
+ )} {/* Instruction Content */}
updateInstruction(index, e.target.value)} - placeholder={!instruction ? "Enter cooking instruction..." : ""} + placeholder={ + !instruction ? "Enter cooking instruction..." : "" + } onFocus={() => setEditingInstructionIndex(index)} onBlur={(e) => { // Only blur if clicking outside this instruction - if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) { + if ( + !e.relatedTarget || + !e.currentTarget.contains(e.relatedTarget as Node) + ) { setEditingInstructionIndex(null); } }} diff --git a/apps/dojo/src/files.json b/apps/dojo/src/files.json index 05088daa1d..df809932bd 100644 --- a/apps/dojo/src/files.json +++ b/apps/dojo/src/files.json @@ -434,7 +434,7 @@ "langgraph::shared_state": [ { "name": "page.tsx", - "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n
\n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n
\n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n
\n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !== undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (agentState.recipe as unknown as Record)[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", + "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const {\n isChatOpen,\n setChatHeight,\n setIsChatOpen,\n isDragging,\n chatHeight,\n handleDragStart,\n } = useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
\n {chatDescription}\n
\n
\n
\n \n \n \n \n
\n
\n
\n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n
\n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n
\n\n {/* Backdrop */}\n {isChatOpen && (\n setIsChatOpen(false)}\n />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n const isAgentReady = Boolean(copilotkit.getAgent(\"shared_state\"));\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState<\n number | null\n >(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !==\n undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (\n agentState.recipe as unknown as Record\n )[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] =\n agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter(\n (p) => p !== preference,\n ),\n });\n }\n };\n\n const handleCookingTimeChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [\n ...recipe.ingredients,\n { icon: \"🍴\", name: \"\", amount: \"\" },\n ],\n });\n };\n\n const updateIngredient = (\n index: number,\n field: keyof Ingredient,\n value: string,\n ) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\n \".instructions-container textarea\",\n );\n const newTextarea = textareas[\n textareas.length - 1\n ] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)\n ?.value || 3\n }\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n \n {recipe.ingredients.map((ingredient, index) => (\n \n
\n {getProperIcon(ingredient.icon)}\n
\n
\n \n updateIngredient(index, \"name\", e.target.value)\n }\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n \n updateIngredient(index, \"amount\", e.target.value)\n }\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n \n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n + Add Step\n \n
\n \n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 && (\n
\n )}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={\n !instruction ? \"Enter cooking instruction...\" : \"\"\n }\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (\n !e.relatedTarget ||\n !e.currentTarget.contains(e.relatedTarget as Node)\n ) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n \n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", "language": "typescript", "type": "file" }, @@ -792,7 +792,7 @@ "langgraph-fastapi::shared_state": [ { "name": "page.tsx", - "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !== undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (agentState.recipe as unknown as Record)[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", + "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const {\n isChatOpen,\n setChatHeight,\n setIsChatOpen,\n isDragging,\n chatHeight,\n handleDragStart,\n } = useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
\n {chatDescription}\n
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n setIsChatOpen(false)}\n />\n )}\n \n ) : (\n \n )}\n \n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n const isAgentReady = Boolean(copilotkit.getAgent(\"shared_state\"));\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState<\n number | null\n >(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !==\n undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (\n agentState.recipe as unknown as Record\n )[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] =\n agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter(\n (p) => p !== preference,\n ),\n });\n }\n };\n\n const handleCookingTimeChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [\n ...recipe.ingredients,\n { icon: \"🍴\", name: \"\", amount: \"\" },\n ],\n });\n };\n\n const updateIngredient = (\n index: number,\n field: keyof Ingredient,\n value: string,\n ) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\n \".instructions-container textarea\",\n );\n const newTextarea = textareas[\n textareas.length - 1\n ] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)\n ?.value || 3\n }\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n \n {recipe.ingredients.map((ingredient, index) => (\n \n
\n {getProperIcon(ingredient.icon)}\n
\n
\n \n updateIngredient(index, \"name\", e.target.value)\n }\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n \n updateIngredient(index, \"amount\", e.target.value)\n }\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n \n \n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n + Add Step\n \n
\n \n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 && (\n
\n )}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={\n !instruction ? \"Enter cooking instruction...\" : \"\"\n }\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (\n !e.relatedTarget ||\n !e.currentTarget.contains(e.relatedTarget as Node)\n ) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n \n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", "language": "typescript", "type": "file" }, @@ -1130,7 +1130,7 @@ "langgraph-typescript::shared_state": [ { "name": "page.tsx", - "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !== undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (agentState.recipe as unknown as Record)[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", + "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const {\n isChatOpen,\n setChatHeight,\n setIsChatOpen,\n isDragging,\n chatHeight,\n handleDragStart,\n } = useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
\n {chatDescription}\n
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n setIsChatOpen(false)}\n />\n )}\n \n ) : (\n \n )}\n \n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n const isAgentReady = Boolean(copilotkit.getAgent(\"shared_state\"));\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState<\n number | null\n >(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !==\n undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (\n agentState.recipe as unknown as Record\n )[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] =\n agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter(\n (p) => p !== preference,\n ),\n });\n }\n };\n\n const handleCookingTimeChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [\n ...recipe.ingredients,\n { icon: \"🍴\", name: \"\", amount: \"\" },\n ],\n });\n };\n\n const updateIngredient = (\n index: number,\n field: keyof Ingredient,\n value: string,\n ) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\n \".instructions-container textarea\",\n );\n const newTextarea = textareas[\n textareas.length - 1\n ] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)\n ?.value || 3\n }\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n \n {recipe.ingredients.map((ingredient, index) => (\n \n
\n {getProperIcon(ingredient.icon)}\n
\n
\n \n updateIngredient(index, \"name\", e.target.value)\n }\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n \n updateIngredient(index, \"amount\", e.target.value)\n }\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n \n \n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n + Add Step\n \n
\n \n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 && (\n
\n )}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={\n !instruction ? \"Enter cooking instruction...\" : \"\"\n }\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (\n !e.relatedTarget ||\n !e.currentTarget.contains(e.relatedTarget as Node)\n ) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n \n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", "language": "typescript", "type": "file" }, @@ -1476,7 +1476,7 @@ "mastra::shared_state": [ { "name": "page.tsx", - "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !== undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (agentState.recipe as unknown as Record)[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", + "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const {\n isChatOpen,\n setChatHeight,\n setIsChatOpen,\n isDragging,\n chatHeight,\n handleDragStart,\n } = useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
\n {chatDescription}\n
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n setIsChatOpen(false)}\n />\n )}\n \n ) : (\n \n )}\n \n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n const isAgentReady = Boolean(copilotkit.getAgent(\"shared_state\"));\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState<\n number | null\n >(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !==\n undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (\n agentState.recipe as unknown as Record\n )[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] =\n agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter(\n (p) => p !== preference,\n ),\n });\n }\n };\n\n const handleCookingTimeChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [\n ...recipe.ingredients,\n { icon: \"🍴\", name: \"\", amount: \"\" },\n ],\n });\n };\n\n const updateIngredient = (\n index: number,\n field: keyof Ingredient,\n value: string,\n ) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\n \".instructions-container textarea\",\n );\n const newTextarea = textareas[\n textareas.length - 1\n ] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)\n ?.value || 3\n }\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n \n {recipe.ingredients.map((ingredient, index) => (\n \n
\n {getProperIcon(ingredient.icon)}\n
\n
\n \n updateIngredient(index, \"name\", e.target.value)\n }\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n \n updateIngredient(index, \"amount\", e.target.value)\n }\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n \n \n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n + Add Step\n \n
\n \n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 && (\n
\n )}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={\n !instruction ? \"Enter cooking instruction...\" : \"\"\n }\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (\n !e.relatedTarget ||\n !e.currentTarget.contains(e.relatedTarget as Node)\n ) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n \n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", "language": "typescript", "type": "file" }, @@ -1720,7 +1720,7 @@ "mastra-agent-local::shared_state": [ { "name": "page.tsx", - "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !== undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (agentState.recipe as unknown as Record)[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", + "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const {\n isChatOpen,\n setChatHeight,\n setIsChatOpen,\n isDragging,\n chatHeight,\n handleDragStart,\n } = useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
\n {chatDescription}\n
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n setIsChatOpen(false)}\n />\n )}\n \n ) : (\n \n )}\n \n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n const isAgentReady = Boolean(copilotkit.getAgent(\"shared_state\"));\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState<\n number | null\n >(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !==\n undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (\n agentState.recipe as unknown as Record\n )[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] =\n agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter(\n (p) => p !== preference,\n ),\n });\n }\n };\n\n const handleCookingTimeChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [\n ...recipe.ingredients,\n { icon: \"🍴\", name: \"\", amount: \"\" },\n ],\n });\n };\n\n const updateIngredient = (\n index: number,\n field: keyof Ingredient,\n value: string,\n ) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\n \".instructions-container textarea\",\n );\n const newTextarea = textareas[\n textareas.length - 1\n ] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)\n ?.value || 3\n }\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n \n {recipe.ingredients.map((ingredient, index) => (\n \n
\n {getProperIcon(ingredient.icon)}\n
\n
\n \n updateIngredient(index, \"name\", e.target.value)\n }\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n \n updateIngredient(index, \"amount\", e.target.value)\n }\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n \n \n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n + Add Step\n \n
\n \n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 && (\n
\n )}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={\n !instruction ? \"Enter cooking instruction...\" : \"\"\n }\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (\n !e.relatedTarget ||\n !e.currentTarget.contains(e.relatedTarget as Node)\n ) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n \n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", "language": "typescript", "type": "file" }, @@ -1906,7 +1906,7 @@ "spring-ai::shared_state": [ { "name": "page.tsx", - "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !== undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (agentState.recipe as unknown as Record)[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", + "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const {\n isChatOpen,\n setChatHeight,\n setIsChatOpen,\n isDragging,\n chatHeight,\n handleDragStart,\n } = useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
\n {chatDescription}\n
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n setIsChatOpen(false)}\n />\n )}\n \n ) : (\n \n )}\n \n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n const isAgentReady = Boolean(copilotkit.getAgent(\"shared_state\"));\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState<\n number | null\n >(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !==\n undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (\n agentState.recipe as unknown as Record\n )[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] =\n agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter(\n (p) => p !== preference,\n ),\n });\n }\n };\n\n const handleCookingTimeChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [\n ...recipe.ingredients,\n { icon: \"🍴\", name: \"\", amount: \"\" },\n ],\n });\n };\n\n const updateIngredient = (\n index: number,\n field: keyof Ingredient,\n value: string,\n ) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\n \".instructions-container textarea\",\n );\n const newTextarea = textareas[\n textareas.length - 1\n ] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)\n ?.value || 3\n }\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n \n {recipe.ingredients.map((ingredient, index) => (\n \n
\n {getProperIcon(ingredient.icon)}\n
\n
\n \n updateIngredient(index, \"name\", e.target.value)\n }\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n \n updateIngredient(index, \"amount\", e.target.value)\n }\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n \n \n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n + Add Step\n \n
\n \n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 && (\n
\n )}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={\n !instruction ? \"Enter cooking instruction...\" : \"\"\n }\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (\n !e.relatedTarget ||\n !e.currentTarget.contains(e.relatedTarget as Node)\n ) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n \n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", "language": "typescript", "type": "file" }, @@ -2106,7 +2106,7 @@ "pydantic-ai::shared_state": [ { "name": "page.tsx", - "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !== undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (agentState.recipe as unknown as Record)[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", + "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const {\n isChatOpen,\n setChatHeight,\n setIsChatOpen,\n isDragging,\n chatHeight,\n handleDragStart,\n } = useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
\n {chatDescription}\n
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n setIsChatOpen(false)}\n />\n )}\n \n ) : (\n \n )}\n \n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n const isAgentReady = Boolean(copilotkit.getAgent(\"shared_state\"));\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState<\n number | null\n >(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !==\n undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (\n agentState.recipe as unknown as Record\n )[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] =\n agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter(\n (p) => p !== preference,\n ),\n });\n }\n };\n\n const handleCookingTimeChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [\n ...recipe.ingredients,\n { icon: \"🍴\", name: \"\", amount: \"\" },\n ],\n });\n };\n\n const updateIngredient = (\n index: number,\n field: keyof Ingredient,\n value: string,\n ) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\n \".instructions-container textarea\",\n );\n const newTextarea = textareas[\n textareas.length - 1\n ] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)\n ?.value || 3\n }\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n \n {recipe.ingredients.map((ingredient, index) => (\n \n
\n {getProperIcon(ingredient.icon)}\n
\n
\n \n updateIngredient(index, \"name\", e.target.value)\n }\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n \n updateIngredient(index, \"amount\", e.target.value)\n }\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n \n \n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n + Add Step\n \n
\n \n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 && (\n
\n )}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={\n !instruction ? \"Enter cooking instruction...\" : \"\"\n }\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (\n !e.relatedTarget ||\n !e.currentTarget.contains(e.relatedTarget as Node)\n ) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n \n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", "language": "typescript", "type": "file" }, @@ -2264,7 +2264,7 @@ "adk-middleware::shared_state": [ { "name": "page.tsx", - "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !== undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (agentState.recipe as unknown as Record)[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", + "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const {\n isChatOpen,\n setChatHeight,\n setIsChatOpen,\n isDragging,\n chatHeight,\n handleDragStart,\n } = useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
\n {chatDescription}\n
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n setIsChatOpen(false)}\n />\n )}\n \n ) : (\n \n )}\n \n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n const isAgentReady = Boolean(copilotkit.getAgent(\"shared_state\"));\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState<\n number | null\n >(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !==\n undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (\n agentState.recipe as unknown as Record\n )[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] =\n agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter(\n (p) => p !== preference,\n ),\n });\n }\n };\n\n const handleCookingTimeChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [\n ...recipe.ingredients,\n { icon: \"🍴\", name: \"\", amount: \"\" },\n ],\n });\n };\n\n const updateIngredient = (\n index: number,\n field: keyof Ingredient,\n value: string,\n ) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\n \".instructions-container textarea\",\n );\n const newTextarea = textareas[\n textareas.length - 1\n ] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)\n ?.value || 3\n }\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n \n {recipe.ingredients.map((ingredient, index) => (\n \n
\n {getProperIcon(ingredient.icon)}\n
\n
\n \n updateIngredient(index, \"name\", e.target.value)\n }\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n \n updateIngredient(index, \"amount\", e.target.value)\n }\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n \n \n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n + Add Step\n \n
\n \n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 && (\n
\n )}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={\n !instruction ? \"Enter cooking instruction...\" : \"\"\n }\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (\n !e.relatedTarget ||\n !e.currentTarget.contains(e.relatedTarget as Node)\n ) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n \n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", "language": "typescript", "type": "file" }, @@ -2568,7 +2568,7 @@ "microsoft-agent-framework-dotnet::shared_state": [ { "name": "page.tsx", - "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !== undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (agentState.recipe as unknown as Record)[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", + "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const {\n isChatOpen,\n setChatHeight,\n setIsChatOpen,\n isDragging,\n chatHeight,\n handleDragStart,\n } = useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
\n {chatDescription}\n
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n setIsChatOpen(false)}\n />\n )}\n \n ) : (\n \n )}\n \n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n const isAgentReady = Boolean(copilotkit.getAgent(\"shared_state\"));\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState<\n number | null\n >(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !==\n undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (\n agentState.recipe as unknown as Record\n )[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] =\n agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter(\n (p) => p !== preference,\n ),\n });\n }\n };\n\n const handleCookingTimeChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [\n ...recipe.ingredients,\n { icon: \"🍴\", name: \"\", amount: \"\" },\n ],\n });\n };\n\n const updateIngredient = (\n index: number,\n field: keyof Ingredient,\n value: string,\n ) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\n \".instructions-container textarea\",\n );\n const newTextarea = textareas[\n textareas.length - 1\n ] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)\n ?.value || 3\n }\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n \n {recipe.ingredients.map((ingredient, index) => (\n \n
\n {getProperIcon(ingredient.icon)}\n
\n
\n \n updateIngredient(index, \"name\", e.target.value)\n }\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n \n updateIngredient(index, \"amount\", e.target.value)\n }\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n \n \n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n + Add Step\n \n
\n \n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 && (\n
\n )}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={\n !instruction ? \"Enter cooking instruction...\" : \"\"\n }\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (\n !e.relatedTarget ||\n !e.currentTarget.contains(e.relatedTarget as Node)\n ) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n \n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", "language": "typescript", "type": "file" }, @@ -2838,7 +2838,7 @@ "ag-ui-dotnet::shared_state": [ { "name": "page.tsx", - "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !== undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (agentState.recipe as unknown as Record)[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", + "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const {\n isChatOpen,\n setChatHeight,\n setIsChatOpen,\n isDragging,\n chatHeight,\n handleDragStart,\n } = useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
\n {chatDescription}\n
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n setIsChatOpen(false)}\n />\n )}\n \n ) : (\n \n )}\n \n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n const isAgentReady = Boolean(copilotkit.getAgent(\"shared_state\"));\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState<\n number | null\n >(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !==\n undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (\n agentState.recipe as unknown as Record\n )[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] =\n agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter(\n (p) => p !== preference,\n ),\n });\n }\n };\n\n const handleCookingTimeChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [\n ...recipe.ingredients,\n { icon: \"🍴\", name: \"\", amount: \"\" },\n ],\n });\n };\n\n const updateIngredient = (\n index: number,\n field: keyof Ingredient,\n value: string,\n ) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\n \".instructions-container textarea\",\n );\n const newTextarea = textareas[\n textareas.length - 1\n ] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)\n ?.value || 3\n }\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n \n {recipe.ingredients.map((ingredient, index) => (\n \n
\n {getProperIcon(ingredient.icon)}\n
\n
\n \n updateIngredient(index, \"name\", e.target.value)\n }\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n \n updateIngredient(index, \"amount\", e.target.value)\n }\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n \n \n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n + Add Step\n \n
\n \n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 && (\n
\n )}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={\n !instruction ? \"Enter cooking instruction...\" : \"\"\n }\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (\n !e.relatedTarget ||\n !e.currentTarget.contains(e.relatedTarget as Node)\n ) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n \n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", "language": "typescript", "type": "file" }, @@ -3188,7 +3188,7 @@ "microsoft-agent-framework-python::shared_state": [ { "name": "page.tsx", - "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !== undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (agentState.recipe as unknown as Record)[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", + "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const {\n isChatOpen,\n setChatHeight,\n setIsChatOpen,\n isDragging,\n chatHeight,\n handleDragStart,\n } = useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
\n {chatDescription}\n
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n setIsChatOpen(false)}\n />\n )}\n \n ) : (\n \n )}\n \n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n const isAgentReady = Boolean(copilotkit.getAgent(\"shared_state\"));\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState<\n number | null\n >(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !==\n undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (\n agentState.recipe as unknown as Record\n )[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] =\n agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter(\n (p) => p !== preference,\n ),\n });\n }\n };\n\n const handleCookingTimeChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [\n ...recipe.ingredients,\n { icon: \"🍴\", name: \"\", amount: \"\" },\n ],\n });\n };\n\n const updateIngredient = (\n index: number,\n field: keyof Ingredient,\n value: string,\n ) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\n \".instructions-container textarea\",\n );\n const newTextarea = textareas[\n textareas.length - 1\n ] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)\n ?.value || 3\n }\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n \n {recipe.ingredients.map((ingredient, index) => (\n \n
\n {getProperIcon(ingredient.icon)}\n
\n
\n \n updateIngredient(index, \"name\", e.target.value)\n }\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n \n updateIngredient(index, \"amount\", e.target.value)\n }\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n \n \n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n + Add Step\n \n
\n \n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 && (\n
\n )}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={\n !instruction ? \"Enter cooking instruction...\" : \"\"\n }\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (\n !e.relatedTarget ||\n !e.currentTarget.contains(e.relatedTarget as Node)\n ) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n \n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", "language": "typescript", "type": "file" }, @@ -3470,7 +3470,7 @@ "ag2::shared_state": [ { "name": "page.tsx", - "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !== undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (agentState.recipe as unknown as Record)[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", + "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const {\n isChatOpen,\n setChatHeight,\n setIsChatOpen,\n isDragging,\n chatHeight,\n handleDragStart,\n } = useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
\n {chatDescription}\n
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n setIsChatOpen(false)}\n />\n )}\n \n ) : (\n \n )}\n \n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n const isAgentReady = Boolean(copilotkit.getAgent(\"shared_state\"));\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState<\n number | null\n >(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !==\n undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (\n agentState.recipe as unknown as Record\n )[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] =\n agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter(\n (p) => p !== preference,\n ),\n });\n }\n };\n\n const handleCookingTimeChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [\n ...recipe.ingredients,\n { icon: \"🍴\", name: \"\", amount: \"\" },\n ],\n });\n };\n\n const updateIngredient = (\n index: number,\n field: keyof Ingredient,\n value: string,\n ) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\n \".instructions-container textarea\",\n );\n const newTextarea = textareas[\n textareas.length - 1\n ] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)\n ?.value || 3\n }\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n \n {recipe.ingredients.map((ingredient, index) => (\n \n
\n {getProperIcon(ingredient.icon)}\n
\n
\n \n updateIngredient(index, \"name\", e.target.value)\n }\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n \n updateIngredient(index, \"amount\", e.target.value)\n }\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n \n \n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n + Add Step\n \n
\n \n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 && (\n
\n )}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={\n !instruction ? \"Enter cooking instruction...\" : \"\"\n }\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (\n !e.relatedTarget ||\n !e.currentTarget.contains(e.relatedTarget as Node)\n ) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n \n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", "language": "typescript", "type": "file" }, @@ -3700,7 +3700,7 @@ "agno::shared_state": [ { "name": "page.tsx", - "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !== undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (agentState.recipe as unknown as Record)[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", + "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const {\n isChatOpen,\n setChatHeight,\n setIsChatOpen,\n isDragging,\n chatHeight,\n handleDragStart,\n } = useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
\n {chatDescription}\n
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n setIsChatOpen(false)}\n />\n )}\n \n ) : (\n \n )}\n \n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n const isAgentReady = Boolean(copilotkit.getAgent(\"shared_state\"));\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState<\n number | null\n >(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !==\n undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (\n agentState.recipe as unknown as Record\n )[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] =\n agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter(\n (p) => p !== preference,\n ),\n });\n }\n };\n\n const handleCookingTimeChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [\n ...recipe.ingredients,\n { icon: \"🍴\", name: \"\", amount: \"\" },\n ],\n });\n };\n\n const updateIngredient = (\n index: number,\n field: keyof Ingredient,\n value: string,\n ) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\n \".instructions-container textarea\",\n );\n const newTextarea = textareas[\n textareas.length - 1\n ] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)\n ?.value || 3\n }\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n \n {recipe.ingredients.map((ingredient, index) => (\n \n
\n {getProperIcon(ingredient.icon)}\n
\n
\n \n updateIngredient(index, \"name\", e.target.value)\n }\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n \n updateIngredient(index, \"amount\", e.target.value)\n }\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n \n \n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n + Add Step\n \n
\n \n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 && (\n
\n )}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={\n !instruction ? \"Enter cooking instruction...\" : \"\"\n }\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (\n !e.relatedTarget ||\n !e.currentTarget.contains(e.relatedTarget as Node)\n ) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n \n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", "language": "typescript", "type": "file" }, @@ -3878,7 +3878,7 @@ "llama-index::shared_state": [ { "name": "page.tsx", - "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !== undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (agentState.recipe as unknown as Record)[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", + "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const {\n isChatOpen,\n setChatHeight,\n setIsChatOpen,\n isDragging,\n chatHeight,\n handleDragStart,\n } = useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
\n {chatDescription}\n
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n setIsChatOpen(false)}\n />\n )}\n \n ) : (\n \n )}\n \n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n const isAgentReady = Boolean(copilotkit.getAgent(\"shared_state\"));\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState<\n number | null\n >(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !==\n undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (\n agentState.recipe as unknown as Record\n )[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] =\n agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter(\n (p) => p !== preference,\n ),\n });\n }\n };\n\n const handleCookingTimeChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [\n ...recipe.ingredients,\n { icon: \"🍴\", name: \"\", amount: \"\" },\n ],\n });\n };\n\n const updateIngredient = (\n index: number,\n field: keyof Ingredient,\n value: string,\n ) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\n \".instructions-container textarea\",\n );\n const newTextarea = textareas[\n textareas.length - 1\n ] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)\n ?.value || 3\n }\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n \n {recipe.ingredients.map((ingredient, index) => (\n \n
\n {getProperIcon(ingredient.icon)}\n
\n
\n \n updateIngredient(index, \"name\", e.target.value)\n }\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n \n updateIngredient(index, \"amount\", e.target.value)\n }\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n \n \n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n + Add Step\n \n
\n \n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 && (\n
\n )}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={\n !instruction ? \"Enter cooking instruction...\" : \"\"\n }\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (\n !e.relatedTarget ||\n !e.currentTarget.contains(e.relatedTarget as Node)\n ) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n \n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", "language": "typescript", "type": "file" }, @@ -4096,7 +4096,7 @@ "crewai::shared_state": [ { "name": "page.tsx", - "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !== undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (agentState.recipe as unknown as Record)[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", + "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const {\n isChatOpen,\n setChatHeight,\n setIsChatOpen,\n isDragging,\n chatHeight,\n handleDragStart,\n } = useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
\n {chatDescription}\n
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n setIsChatOpen(false)}\n />\n )}\n \n ) : (\n \n )}\n \n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n const isAgentReady = Boolean(copilotkit.getAgent(\"shared_state\"));\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState<\n number | null\n >(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !==\n undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (\n agentState.recipe as unknown as Record\n )[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] =\n agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter(\n (p) => p !== preference,\n ),\n });\n }\n };\n\n const handleCookingTimeChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [\n ...recipe.ingredients,\n { icon: \"🍴\", name: \"\", amount: \"\" },\n ],\n });\n };\n\n const updateIngredient = (\n index: number,\n field: keyof Ingredient,\n value: string,\n ) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\n \".instructions-container textarea\",\n );\n const newTextarea = textareas[\n textareas.length - 1\n ] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)\n ?.value || 3\n }\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n \n {recipe.ingredients.map((ingredient, index) => (\n \n
\n {getProperIcon(ingredient.icon)}\n
\n
\n \n updateIngredient(index, \"name\", e.target.value)\n }\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n \n updateIngredient(index, \"amount\", e.target.value)\n }\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n \n \n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n + Add Step\n \n
\n \n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 && (\n
\n )}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={\n !instruction ? \"Enter cooking instruction...\" : \"\"\n }\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (\n !e.relatedTarget ||\n !e.currentTarget.contains(e.relatedTarget as Node)\n ) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n \n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", "language": "typescript", "type": "file" }, @@ -4524,7 +4524,7 @@ "crewai-conversational-flows::shared_state": [ { "name": "page.tsx", - "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !== undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (agentState.recipe as unknown as Record)[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", + "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const {\n isChatOpen,\n setChatHeight,\n setIsChatOpen,\n isDragging,\n chatHeight,\n handleDragStart,\n } = useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
\n {chatDescription}\n
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n setIsChatOpen(false)}\n />\n )}\n \n ) : (\n \n )}\n \n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n const isAgentReady = Boolean(copilotkit.getAgent(\"shared_state\"));\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState<\n number | null\n >(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !==\n undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (\n agentState.recipe as unknown as Record\n )[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] =\n agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter(\n (p) => p !== preference,\n ),\n });\n }\n };\n\n const handleCookingTimeChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [\n ...recipe.ingredients,\n { icon: \"🍴\", name: \"\", amount: \"\" },\n ],\n });\n };\n\n const updateIngredient = (\n index: number,\n field: keyof Ingredient,\n value: string,\n ) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\n \".instructions-container textarea\",\n );\n const newTextarea = textareas[\n textareas.length - 1\n ] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)\n ?.value || 3\n }\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n \n {recipe.ingredients.map((ingredient, index) => (\n \n
\n {getProperIcon(ingredient.icon)}\n
\n
\n \n updateIngredient(index, \"name\", e.target.value)\n }\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n \n updateIngredient(index, \"amount\", e.target.value)\n }\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n \n \n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n + Add Step\n \n
\n \n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 && (\n
\n )}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={\n !instruction ? \"Enter cooking instruction...\" : \"\"\n }\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (\n !e.relatedTarget ||\n !e.currentTarget.contains(e.relatedTarget as Node)\n ) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n \n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", "language": "typescript", "type": "file" }, @@ -4884,7 +4884,7 @@ "server-starter-all-features::shared_state": [ { "name": "page.tsx", - "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !== undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (agentState.recipe as unknown as Record)[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", + "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const {\n isChatOpen,\n setChatHeight,\n setIsChatOpen,\n isDragging,\n chatHeight,\n handleDragStart,\n } = useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
\n {chatDescription}\n
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n setIsChatOpen(false)}\n />\n )}\n \n ) : (\n \n )}\n \n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n const isAgentReady = Boolean(copilotkit.getAgent(\"shared_state\"));\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState<\n number | null\n >(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !==\n undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (\n agentState.recipe as unknown as Record\n )[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] =\n agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter(\n (p) => p !== preference,\n ),\n });\n }\n };\n\n const handleCookingTimeChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [\n ...recipe.ingredients,\n { icon: \"🍴\", name: \"\", amount: \"\" },\n ],\n });\n };\n\n const updateIngredient = (\n index: number,\n field: keyof Ingredient,\n value: string,\n ) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\n \".instructions-container textarea\",\n );\n const newTextarea = textareas[\n textareas.length - 1\n ] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)\n ?.value || 3\n }\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n \n {recipe.ingredients.map((ingredient, index) => (\n \n
\n {getProperIcon(ingredient.icon)}\n
\n
\n \n updateIngredient(index, \"name\", e.target.value)\n }\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n \n updateIngredient(index, \"amount\", e.target.value)\n }\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n \n \n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n + Add Step\n \n
\n \n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 && (\n
\n )}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={\n !instruction ? \"Enter cooking instruction...\" : \"\"\n }\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (\n !e.relatedTarget ||\n !e.currentTarget.contains(e.relatedTarget as Node)\n ) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n \n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", "language": "typescript", "type": "file" }, @@ -5088,7 +5088,7 @@ "aws-strands::shared_state": [ { "name": "page.tsx", - "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !== undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (agentState.recipe as unknown as Record)[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", + "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const {\n isChatOpen,\n setChatHeight,\n setIsChatOpen,\n isDragging,\n chatHeight,\n handleDragStart,\n } = useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
\n {chatDescription}\n
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n setIsChatOpen(false)}\n />\n )}\n \n ) : (\n \n )}\n \n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n const isAgentReady = Boolean(copilotkit.getAgent(\"shared_state\"));\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState<\n number | null\n >(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !==\n undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (\n agentState.recipe as unknown as Record\n )[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] =\n agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter(\n (p) => p !== preference,\n ),\n });\n }\n };\n\n const handleCookingTimeChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [\n ...recipe.ingredients,\n { icon: \"🍴\", name: \"\", amount: \"\" },\n ],\n });\n };\n\n const updateIngredient = (\n index: number,\n field: keyof Ingredient,\n value: string,\n ) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\n \".instructions-container textarea\",\n );\n const newTextarea = textareas[\n textareas.length - 1\n ] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)\n ?.value || 3\n }\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n \n {recipe.ingredients.map((ingredient, index) => (\n \n
\n {getProperIcon(ingredient.icon)}\n
\n
\n \n updateIngredient(index, \"name\", e.target.value)\n }\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n \n updateIngredient(index, \"amount\", e.target.value)\n }\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n \n \n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n + Add Step\n \n
\n \n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 && (\n
\n )}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={\n !instruction ? \"Enter cooking instruction...\" : \"\"\n }\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (\n !e.relatedTarget ||\n !e.currentTarget.contains(e.relatedTarget as Node)\n ) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n \n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", "language": "typescript", "type": "file" }, @@ -5476,7 +5476,7 @@ "aws-strands-typescript::shared_state": [ { "name": "page.tsx", - "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !== undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (agentState.recipe as unknown as Record)[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", + "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const {\n isChatOpen,\n setChatHeight,\n setIsChatOpen,\n isDragging,\n chatHeight,\n handleDragStart,\n } = useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
\n {chatDescription}\n
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n setIsChatOpen(false)}\n />\n )}\n \n ) : (\n \n )}\n \n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n const isAgentReady = Boolean(copilotkit.getAgent(\"shared_state\"));\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState<\n number | null\n >(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !==\n undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (\n agentState.recipe as unknown as Record\n )[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] =\n agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter(\n (p) => p !== preference,\n ),\n });\n }\n };\n\n const handleCookingTimeChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [\n ...recipe.ingredients,\n { icon: \"🍴\", name: \"\", amount: \"\" },\n ],\n });\n };\n\n const updateIngredient = (\n index: number,\n field: keyof Ingredient,\n value: string,\n ) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\n \".instructions-container textarea\",\n );\n const newTextarea = textareas[\n textareas.length - 1\n ] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)\n ?.value || 3\n }\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n \n {recipe.ingredients.map((ingredient, index) => (\n \n
\n {getProperIcon(ingredient.icon)}\n
\n
\n \n updateIngredient(index, \"name\", e.target.value)\n }\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n \n updateIngredient(index, \"amount\", e.target.value)\n }\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n \n \n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n + Add Step\n \n
\n \n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 && (\n
\n )}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={\n !instruction ? \"Enter cooking instruction...\" : \"\"\n }\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (\n !e.relatedTarget ||\n !e.currentTarget.contains(e.relatedTarget as Node)\n ) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n \n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", "language": "typescript", "type": "file" }, @@ -5758,7 +5758,7 @@ "claude-agent-sdk-python::shared_state": [ { "name": "page.tsx", - "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !== undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (agentState.recipe as unknown as Record)[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", + "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const {\n isChatOpen,\n setChatHeight,\n setIsChatOpen,\n isDragging,\n chatHeight,\n handleDragStart,\n } = useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
\n {chatDescription}\n
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n setIsChatOpen(false)}\n />\n )}\n \n ) : (\n \n )}\n \n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n const isAgentReady = Boolean(copilotkit.getAgent(\"shared_state\"));\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState<\n number | null\n >(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !==\n undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (\n agentState.recipe as unknown as Record\n )[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] =\n agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter(\n (p) => p !== preference,\n ),\n });\n }\n };\n\n const handleCookingTimeChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [\n ...recipe.ingredients,\n { icon: \"🍴\", name: \"\", amount: \"\" },\n ],\n });\n };\n\n const updateIngredient = (\n index: number,\n field: keyof Ingredient,\n value: string,\n ) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\n \".instructions-container textarea\",\n );\n const newTextarea = textareas[\n textareas.length - 1\n ] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)\n ?.value || 3\n }\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n \n {recipe.ingredients.map((ingredient, index) => (\n \n
\n {getProperIcon(ingredient.icon)}\n
\n
\n \n updateIngredient(index, \"name\", e.target.value)\n }\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n \n updateIngredient(index, \"amount\", e.target.value)\n }\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n \n \n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n + Add Step\n \n
\n \n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 && (\n
\n )}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={\n !instruction ? \"Enter cooking instruction...\" : \"\"\n }\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (\n !e.relatedTarget ||\n !e.currentTarget.contains(e.relatedTarget as Node)\n ) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n \n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", "language": "typescript", "type": "file" }, @@ -5876,7 +5876,7 @@ "claude-agent-sdk-typescript::shared_state": [ { "name": "page.tsx", - "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !== undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (agentState.recipe as unknown as Record)[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", + "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const {\n isChatOpen,\n setChatHeight,\n setIsChatOpen,\n isDragging,\n chatHeight,\n handleDragStart,\n } = useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
\n {chatDescription}\n
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n setIsChatOpen(false)}\n />\n )}\n \n ) : (\n \n )}\n \n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n const isAgentReady = Boolean(copilotkit.getAgent(\"shared_state\"));\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState<\n number | null\n >(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !==\n undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (\n agentState.recipe as unknown as Record\n )[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] =\n agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter(\n (p) => p !== preference,\n ),\n });\n }\n };\n\n const handleCookingTimeChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [\n ...recipe.ingredients,\n { icon: \"🍴\", name: \"\", amount: \"\" },\n ],\n });\n };\n\n const updateIngredient = (\n index: number,\n field: keyof Ingredient,\n value: string,\n ) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\n \".instructions-container textarea\",\n );\n const newTextarea = textareas[\n textareas.length - 1\n ] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)\n ?.value || 3\n }\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n \n {recipe.ingredients.map((ingredient, index) => (\n \n
\n {getProperIcon(ingredient.icon)}\n
\n
\n \n updateIngredient(index, \"name\", e.target.value)\n }\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n \n updateIngredient(index, \"amount\", e.target.value)\n }\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n \n \n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n + Add Step\n \n
\n \n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 && (\n
\n )}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={\n !instruction ? \"Enter cooking instruction...\" : \"\"\n }\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (\n !e.relatedTarget ||\n !e.currentTarget.contains(e.relatedTarget as Node)\n ) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n \n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", "language": "typescript", "type": "file" }, @@ -6392,7 +6392,7 @@ "langroid::shared_state": [ { "name": "page.tsx", - "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !== undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (agentState.recipe as unknown as Record)[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", + "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const {\n isChatOpen,\n setChatHeight,\n setIsChatOpen,\n isDragging,\n chatHeight,\n handleDragStart,\n } = useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
\n {chatDescription}\n
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n setIsChatOpen(false)}\n />\n )}\n \n ) : (\n \n )}\n \n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n const isAgentReady = Boolean(copilotkit.getAgent(\"shared_state\"));\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n // DEFERRED (PNI-307): mount-only by design — this seeds the agent state\n // exactly once. Adding `agentState?.recipe`/`setAgentState` would re-seed\n // INITIAL_STATE whenever the agent clears its recipe mid-session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState<\n number | null\n >(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n // NOTE (PNI-272): this stays a ref, written and read within the same render,\n // and the `setRecipe` sync below stays in an effect. Converting either to a\n // render-phase state update loses the Ping entirely whenever the agent's final\n // recipe change arrives with `isLoading` already false: the retry render sees\n // the recipe already synchronised and `!isLoading`, clears the changed keys and\n // commits without ever showing the indicator. The ref writes the value the same\n // render reads, so the indicator always commits at least once.\n const changedKeysRef = useRef([]);\n\n // Iterating Recipe by dynamic key needs an index-signature view of it. The\n // casts are repeated per access rather than hoisted: hoisting would read\n // `agentState.recipe` once per render where this reads it per key, which is\n // observable if that property is ever accessor- or Proxy-backed.\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as unknown as Record)[key] !==\n undefined &&\n (agentState.recipe as unknown as Record)[key] !== null\n ) {\n let agentValue = (\n agentState.recipe as unknown as Record\n )[key];\n const recipeValue = (recipe as unknown as Record)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as unknown as Record)[key] =\n agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n // DEFERRED (PNI-307): the dep is deliberately the serialized snapshot, not\n // `newRecipeState` — the object is rebuilt every render, so depending on\n // its identity would re-run the sync (and clear the Ping described in the\n // NOTE above) on every render instead of only when the content changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter(\n (p) => p !== preference,\n ),\n });\n }\n };\n\n const handleCookingTimeChange = (\n event: React.ChangeEvent,\n ) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [\n ...recipe.ingredients,\n { icon: \"🍴\", name: \"\", amount: \"\" },\n ],\n });\n };\n\n const updateIngredient = (\n index: number,\n field: keyof Ingredient,\n value: string,\n ) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\n \".instructions-container textarea\",\n );\n const newTextarea = textareas[\n textareas.length - 1\n ] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)\n ?.value || 3\n }\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n \n {recipe.ingredients.map((ingredient, index) => (\n \n
\n {getProperIcon(ingredient.icon)}\n
\n
\n \n updateIngredient(index, \"name\", e.target.value)\n }\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n \n updateIngredient(index, \"amount\", e.target.value)\n }\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n \n \n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n + Add Step\n \n
\n \n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 && (\n
\n )}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={\n !instruction ? \"Enter cooking instruction...\" : \"\"\n }\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (\n !e.relatedTarget ||\n !e.currentTarget.contains(e.relatedTarget as Node)\n ) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n \n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", "language": "typescript", "type": "file" }, diff --git a/integrations/langgraph/typescript/src/agent.ts b/integrations/langgraph/typescript/src/agent.ts index 02b2e374b7..468511a785 100644 --- a/integrations/langgraph/typescript/src/agent.ts +++ b/integrations/langgraph/typescript/src/agent.ts @@ -895,8 +895,10 @@ export class LangGraphAgent extends AbstractAgent { this.activeRun!.prevNodeName = null; let latestStateValues = {} as ThreadState["values"]; - let latestRootStateValues = {} as ThreadState["values"]; - let hasOrderedRootStateValues = false; + // prepareStream's state is the ordered root boundary before any streamed + // chunk, including a first subgraph event that arrives before values mode. + let latestRootStateValues = state.values; + let hasOrderedRootStateValues = true; let updatedState = state; try { @@ -1003,6 +1005,12 @@ export class LangGraphAgent extends AbstractAgent { } const chunkData = chunk.data; + // Once events-mode streaming is active, messages-tuple is a legacy + // fallback only. Skip it before the shared state-snapshot logic so an + // ignored late tuple cannot become a snapshot timing pulse. + if (isMessagesTupleEvent && this.eventsStreamActive) { + continue; + } // messages-tuple chunks arrive as [AIMessageChunk, metadata] arrays; // events-mode chunks are objects with metadata/event properties. Read // metadata from the right slot so langgraph_node is extracted in both @@ -1082,13 +1090,17 @@ export class LangGraphAgent extends AbstractAgent { // LangGraph JS doesn't emit `values` chunks with the latest state between // tool execution and run end, so without this update, intermediate // STATE_SNAPSHOTs go stale after a tool Command updates state. + // A root on_chain_end also advances the ordered root cache: when values + // mode is omitted, the next subgraph boundary snapshots straight from + // that cache, so it must carry the same merged output. if ( eventType === LangGraphEventTypes.OnChainEnd && chunkData.data?.output != null ) { const output: any = chunkData.data.output; + let outputUpdate: Record | undefined; if (typeof output === "object" && !Array.isArray(output)) { - latestStateValues = { ...latestStateValues, ...output }; + outputUpdate = output; } else if (Array.isArray(output)) { for (const item of output) { if ( @@ -1098,13 +1110,19 @@ export class LangGraphAgent extends AbstractAgent { (item as any).update && typeof (item as any).update === "object" ) { - latestStateValues = { - ...latestStateValues, - ...(item as any).update, - }; + outputUpdate = { ...outputUpdate, ...(item as any).update }; } } } + if (outputUpdate) { + latestStateValues = { ...latestStateValues, ...outputUpdate }; + if (currentSubgraph === ROOT_SUBGRAPH_NAME) { + latestRootStateValues = { + ...latestRootStateValues, + ...outputUpdate, + }; + } + } } if ( diff --git a/integrations/langgraph/typescript/src/stream-ordering-races.test.ts b/integrations/langgraph/typescript/src/stream-ordering-races.test.ts new file mode 100644 index 0000000000..9cef7a105f --- /dev/null +++ b/integrations/langgraph/typescript/src/stream-ordering-races.test.ts @@ -0,0 +1,377 @@ +import { + EventType, + type MessagesSnapshotEvent, + type StateSnapshotEvent, +} from "@ag-ui/core"; +// @langchain/langgraph-sdk is a graph persistence client, not an LLM provider; +// aimock does not apply to these synthetic stream-reader tests. +import type { + Assistant, + EventsStreamEvent, + Message as LangGraphMessage, + MessagesTupleStreamEvent, + ThreadState, +} from "@langchain/langgraph-sdk"; +import { describe, expect, it, vi } from "vitest"; +import { LangGraphAgent, type ProcessedEvents } from "./agent"; + +type StreamChunk = EventsStreamEvent | MessagesTupleStreamEvent; + +const TEST_ASSISTANT: Assistant = { + assistant_id: "assistant-1", + graph_id: "test-graph", + config: {}, + context: {}, + created_at: "2026-09-01T00:00:00.000Z", + updated_at: "2026-09-01T00:00:00.000Z", + metadata: {}, + version: 1, + name: "test assistant", +}; + +function threadState(values: Record): ThreadState { + return { + values, + next: [], + checkpoint: { + thread_id: "thread-1", + checkpoint_ns: "", + checkpoint_id: null, + checkpoint_map: null, + }, + metadata: {}, + created_at: null, + parent_checkpoint: null, + tasks: [], + }; +} + +function eventChunk( + event: string, + metadata: Record, + data: unknown, +): EventsStreamEvent { + return { + event: "events", + data: { + event, + name: "test event", + tags: [], + run_id: "run-1", + metadata, + parent_ids: [], + data, + }, + }; +} + +function lateMessageTuple(): MessagesTupleStreamEvent { + const message: LangGraphMessage = { + id: "late-message", + type: "ai", + content: "", + }; + return { + event: "messages", + data: [message, { tags: [], langgraph_node: "tools" }], + }; +} + +async function runUntilStreamError( + agent: LangGraphAgent, + chunks: StreamChunk[], + initialState: ThreadState, +) { + const streamError = new Error("stop after inspected chunk"); + async function* streamResponse(): AsyncGenerator { + yield* chunks; + throw streamError; + } + + agent.assistant = TEST_ASSISTANT; + vi.spyOn(agent, "prepareStream").mockResolvedValue({ + streamResponse: streamResponse(), + state: initialState, + }); + + const events: ProcessedEvents[] = []; + await new Promise((resolve, reject) => { + agent + .run({ + threadId: "thread-1", + runId: "run-1", + state: {}, + messages: [], + tools: [], + context: [], + forwardedProps: {}, + }) + .subscribe({ + next: (event) => events.push(event), + error: (error) => { + if (error === streamError) resolve(); + else reject(error); + }, + complete: () => reject(new Error("stream completed before sentinel")), + }); + }); + return events; +} + +describe("load-dependent stream ordering", () => { + it("does not let a late ignored messages tuple trigger a state snapshot", async () => { + const agent = new LangGraphAgent({ + deploymentUrl: "http://localhost:2024", + graphId: "test-graph", + }); + const state = threadState({ messages: [] }); + + const events = await runUntilStreamError( + agent, + [ + eventChunk( + "on_chat_model_stream", + { langgraph_node: "tools" }, + { chunk: { content: "", response_metadata: {} } }, + ), + eventChunk( + "on_chain_end", + { langgraph_node: "tools" }, + { output: state.values }, + ), + eventChunk( + "on_tool_end", + { langgraph_node: "tools" }, + { + output: { + tool_call_id: "tool-1", + name: "test_tool", + content: "done", + }, + }, + ), + lateMessageTuple(), + ], + state, + ); + + const tupleSnapshots = events.filter( + (event): event is StateSnapshotEvent => + event.type === EventType.STATE_SNAPSHOT && + event.rawEvent?.event === "messages", + ); + expect(tupleSnapshots).toEqual([]); + }); + + it("uses the pre-entry root state when live thread state is ahead", async () => { + const agent = new LangGraphAgent({ + deploymentUrl: "http://localhost:2024", + graphId: "test-graph", + }); + const user: LangGraphMessage = { + id: "user-1", + type: "human", + content: "Plan my trip", + }; + const rootAssistant: LangGraphMessage = { + id: "root-1", + type: "ai", + content: "I will find experiences next", + }; + const futureExperience: LangGraphMessage = { + id: "experience-1", + type: "ai", + content: "Future experiences response", + }; + const preEntryState = threadState({ + messages: [user, rootAssistant], + itinerary: { city: "Amsterdam" }, + }); + const getState = vi + .spyOn(agent.client.threads, "getState") + .mockResolvedValue( + threadState({ + messages: [user, rootAssistant, futureExperience], + itinerary: { + city: "Amsterdam", + experience: "Canal tour", + }, + }), + ); + + const events = await runUntilStreamError( + agent, + [ + eventChunk( + "on_chain_start", + { + langgraph_node: "experiences_agent", + langgraph_checkpoint_ns: + "experiences_agent:outer|experiences_agent_node:inner", + }, + {}, + ), + ], + preEntryState, + ); + + expect(getState).not.toHaveBeenCalled(); + const stateSnapshot = events.find( + (event): event is StateSnapshotEvent => + event.type === EventType.STATE_SNAPSHOT, + ); + expect(stateSnapshot?.snapshot).toEqual(preEntryState.values); + const messagesSnapshot = events.find( + (event): event is MessagesSnapshotEvent => + event.type === EventType.MESSAGES_SNAPSHOT, + ); + expect(messagesSnapshot?.messages.map((message) => message.id)).toEqual([ + "user-1", + "root-1", + ]); + expect( + messagesSnapshot?.messages.map((message) => message.id), + ).not.toContain("experience-1"); + }); + + it("seeds the first subgraph boundary from root on_chain_end object output without a values chunk", async () => { + const agent = new LangGraphAgent({ + deploymentUrl: "http://localhost:2024", + graphId: "test-graph", + }); + const user: LangGraphMessage = { + id: "user-1", + type: "human", + content: "Plan my trip", + }; + const preEntryState = threadState({ + messages: [user], + itinerary: { city: "Amsterdam" }, + }); + const getState = vi + .spyOn(agent.client.threads, "getState") + .mockResolvedValue( + threadState({ + messages: [ + user, + { id: "live-1", type: "ai", content: "Live thread state" }, + ], + itinerary: { city: "Rotterdam" }, + }), + ); + + const events = await runUntilStreamError( + agent, + [ + eventChunk( + "on_chain_end", + { langgraph_node: "planner", langgraph_checkpoint_ns: "" }, + { output: { itinerary: { city: "Amsterdam", hotel: "Hotel Zoe" } } }, + ), + eventChunk( + "on_chain_start", + { + langgraph_node: "experiences_agent", + langgraph_checkpoint_ns: + "experiences_agent:outer|experiences_agent_node:inner", + }, + {}, + ), + ], + preEntryState, + ); + + expect(getState).not.toHaveBeenCalled(); + // The boundary snapshot is the one without a rawEvent; per-chunk node + // change snapshots carry the triggering chunk. + const boundarySnapshot = events.find( + (event): event is StateSnapshotEvent => + event.type === EventType.STATE_SNAPSHOT && event.rawEvent === undefined, + ); + expect(boundarySnapshot?.snapshot).toEqual({ + messages: [user], + itinerary: { city: "Amsterdam", hotel: "Hotel Zoe" }, + }); + const messagesSnapshot = events.find( + (event): event is MessagesSnapshotEvent => + event.type === EventType.MESSAGES_SNAPSHOT, + ); + expect(messagesSnapshot?.messages.map((message) => message.id)).toEqual([ + "user-1", + ]); + }); + + it("seeds the first subgraph boundary from root on_chain_end Command.update without a values chunk", async () => { + const agent = new LangGraphAgent({ + deploymentUrl: "http://localhost:2024", + graphId: "test-graph", + }); + const user: LangGraphMessage = { + id: "user-1", + type: "human", + content: "Plan my trip", + }; + const preEntryState = threadState({ + messages: [user], + itinerary: { city: "Amsterdam" }, + }); + const getState = vi + .spyOn(agent.client.threads, "getState") + .mockResolvedValue( + threadState({ + messages: [ + user, + { id: "live-1", type: "ai", content: "Live thread state" }, + ], + itinerary: { city: "Rotterdam" }, + }), + ); + + const events = await runUntilStreamError( + agent, + [ + eventChunk( + "on_chain_end", + { langgraph_node: "planner", langgraph_checkpoint_ns: "" }, + { + output: [ + { + lg_name: "Command", + update: { + itinerary: { city: "Amsterdam", hotel: "Hotel Zoe" }, + }, + }, + ], + }, + ), + eventChunk( + "on_chain_start", + { + langgraph_node: "experiences_agent", + langgraph_checkpoint_ns: + "experiences_agent:outer|experiences_agent_node:inner", + }, + {}, + ), + ], + preEntryState, + ); + + expect(getState).not.toHaveBeenCalled(); + const boundarySnapshot = events.find( + (event): event is StateSnapshotEvent => + event.type === EventType.STATE_SNAPSHOT && event.rawEvent === undefined, + ); + expect(boundarySnapshot?.snapshot).toEqual({ + messages: [user], + itinerary: { city: "Amsterdam", hotel: "Hotel Zoe" }, + }); + const messagesSnapshot = events.find( + (event): event is MessagesSnapshotEvent => + event.type === EventType.MESSAGES_SNAPSHOT, + ); + expect(messagesSnapshot?.messages.map((message) => message.id)).toEqual([ + "user-1", + ]); + }); +}); diff --git a/integrations/langgraph/typescript/src/subgraph-streaming.test.ts b/integrations/langgraph/typescript/src/subgraph-streaming.test.ts index e3097bae13..2ff32ec261 100644 --- a/integrations/langgraph/typescript/src/subgraph-streaming.test.ts +++ b/integrations/langgraph/typescript/src/subgraph-streaming.test.ts @@ -524,6 +524,7 @@ describe("subgraph change trigger", () => { agent: LangGraphAgent, chunks: any[], expectedError?: Error, + initialStateValues: Record = { messages: [] }, ) { // Patch prepareStream to return our synthetic chunk stream (agent as any).prepareStream = vi.fn().mockResolvedValue({ @@ -533,7 +534,7 @@ describe("subgraph change trigger", () => { yield c; } })(), - state: { values: { messages: [] } } as any, + state: { values: initialStateValues } as any, }); // We call the internal streaming handler directly via run() @@ -622,7 +623,13 @@ describe("subgraph change trigger", () => { }, ]; - await driveAgent(agent, chunks); + await driveAgent(agent, chunks, undefined, { + messages: [ + msg("u1", "human", "AMS to SF"), + msg("f1", "ai", "Booked KLM"), + msg("h1", "ai", "Booked Hotel Zoe"), + ], + }); const snapshots = dispatched.filter( (e) => e?.type === EventType.MESSAGES_SNAPSHOT,