From 12914dd447e215524c3976d3d5bc39dd74563342 Mon Sep 17 00:00:00 2001 From: Ayush8923 <80516839+Ayush8923@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:14:09 +0530 Subject: [PATCH 1/2] feat(*): init the chatbot flow --- app/(main)/chatbot/page.tsx | 74 ++++++++ app/api/chatbot/message/route.ts | 23 +++ app/components/Sidebar.tsx | 8 +- app/components/chatbot/ChatbotFlowCanvas.tsx | 69 +++++++ app/components/chatbot/ChatbotFlowContext.tsx | 23 +++ app/components/chatbot/ChatbotTestPanel.tsx | 119 ++++++++++++ app/components/chatbot/LlmNode.tsx | 88 +++++++++ app/components/chatbot/LlmNodeAdvanced.tsx | 63 +++++++ app/components/chatbot/LlmNodePromptField.tsx | 73 +++++++ app/components/chatbot/NodeFieldLabel.tsx | 22 +++ app/components/chatbot/TerminalNode.tsx | 26 +++ app/components/chatbot/flowHandleStyle.ts | 9 + app/components/chatbot/index.ts | 5 + app/components/icons/index.tsx | 1 + app/components/icons/sidebar/FlowIcon.tsx | 23 +++ app/hooks/useChatbotFlow.ts | 130 +++++++++++++ app/hooks/useChatbotTest.ts | 94 +++++++++ app/lib/chatbotClient.ts | 26 +++ app/lib/constants.ts | 4 + app/lib/modelSchema.ts | 2 + app/lib/navConfig.ts | 6 + app/lib/types/chatbot.ts | 45 +++++ package-lock.json | 178 ++++++++++++++++++ package.json | 1 + 24 files changed, 1107 insertions(+), 5 deletions(-) create mode 100644 app/(main)/chatbot/page.tsx create mode 100644 app/api/chatbot/message/route.ts create mode 100644 app/components/chatbot/ChatbotFlowCanvas.tsx create mode 100644 app/components/chatbot/ChatbotFlowContext.tsx create mode 100644 app/components/chatbot/ChatbotTestPanel.tsx create mode 100644 app/components/chatbot/LlmNode.tsx create mode 100644 app/components/chatbot/LlmNodeAdvanced.tsx create mode 100644 app/components/chatbot/LlmNodePromptField.tsx create mode 100644 app/components/chatbot/NodeFieldLabel.tsx create mode 100644 app/components/chatbot/TerminalNode.tsx create mode 100644 app/components/chatbot/flowHandleStyle.ts create mode 100644 app/components/chatbot/index.ts create mode 100644 app/components/icons/sidebar/FlowIcon.tsx create mode 100644 app/hooks/useChatbotFlow.ts create mode 100644 app/hooks/useChatbotTest.ts create mode 100644 app/lib/chatbotClient.ts create mode 100644 app/lib/types/chatbot.ts diff --git a/app/(main)/chatbot/page.tsx b/app/(main)/chatbot/page.tsx new file mode 100644 index 00000000..720f05b4 --- /dev/null +++ b/app/(main)/chatbot/page.tsx @@ -0,0 +1,74 @@ +/** + * Chatbot - Build a chatbot flow on a canvas and test each step live. + */ + +"use client"; + +import Sidebar from "@/app/components/Sidebar"; +import PageHeader from "@/app/components/PageHeader"; +import { Button } from "@/app/components/ui"; +import { PlusIcon } from "@/app/components/icons"; +import { ChatbotFlowCanvas, ChatbotTestPanel } from "@/app/components/chatbot"; +import { useApp } from "@/app/lib/context/AppContext"; +import { useChatbotFlow } from "@/app/hooks/useChatbotFlow"; + +export default function ChatbotPage() { + const { sidebarCollapsed } = useApp(); + const flow = useChatbotFlow(); + const node = flow.selectedNode; + + return ( +
+
+ + +
+ + + Add step + + } + /> + +
+
+ +
+ + +
+
+
+
+ ); +} diff --git a/app/api/chatbot/message/route.ts b/app/api/chatbot/message/route.ts new file mode 100644 index 00000000..15991d2a --- /dev/null +++ b/app/api/chatbot/message/route.ts @@ -0,0 +1,23 @@ +import { NextResponse } from "next/server"; +import { apiClient } from "@/app/lib/apiClient"; + +export async function POST(request: Request) { + try { + const body = await request.json(); + const { status, data } = await apiClient( + request, + "/api/v1/chatbot/message", + { method: "POST", body: JSON.stringify(body) }, + ); + return NextResponse.json(data, { status }); + } catch (error) { + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : String(error), + data: null, + }, + { status: 500 }, + ); + } +} diff --git a/app/components/Sidebar.tsx b/app/components/Sidebar.tsx index db585568..2ef7443e 100644 --- a/app/components/Sidebar.tsx +++ b/app/components/Sidebar.tsx @@ -1,9 +1,5 @@ -/** - * Sidebar - Navigation sidebar with collapse/expand functionality - * Provides hierarchical navigation with expandable submenus - */ - "use client"; + import React, { useState, useEffect, useRef, useCallback } from "react"; import { useRouter } from "next/navigation"; import Image from "next/image"; @@ -20,6 +16,7 @@ import { ShieldCheckIcon, SlidersIcon, ChatIcon, + FlowIcon, ChartBarIcon, WarningTriangleIcon, } from "@/app/components/icons"; @@ -122,6 +119,7 @@ export default function Sidebar({ const iconMap: Record = { chat: , + flow: , clipboard: , document: , book: , diff --git a/app/components/chatbot/ChatbotFlowCanvas.tsx b/app/components/chatbot/ChatbotFlowCanvas.tsx new file mode 100644 index 00000000..ed3f56f7 --- /dev/null +++ b/app/components/chatbot/ChatbotFlowCanvas.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { useMemo } from "react"; +import { + Background, + BackgroundVariant, + Connection, + Controls, + Edge, + EdgeChange, + NodeChange, + ReactFlow, +} from "@xyflow/react"; +import "@xyflow/react/dist/style.css"; +import LlmNode from "@/app/components/chatbot/LlmNode"; +import TerminalNode from "@/app/components/chatbot/TerminalNode"; +import { ChatbotFlowContext } from "@/app/components/chatbot/ChatbotFlowContext"; +import { ChatbotFlowNode, ChatbotNodeData } from "@/app/lib/types/chatbot"; + +const NODE_TYPES = { + llm: LlmNode, + flowInput: TerminalNode, + flowOutput: TerminalNode, +}; + +const DEFAULT_EDGE_OPTIONS = { style: { stroke: "#a3a3a3", strokeWidth: 1 } }; + +interface ChatbotFlowCanvasProps { + nodes: ChatbotFlowNode[]; + edges: Edge[]; + onNodesChange: (changes: NodeChange[]) => void; + onEdgesChange: (changes: EdgeChange[]) => void; + onConnect: (connection: Connection) => void; + onSelectNode: (id: string) => void; + updateNodeData: (id: string, patch: Partial) => void; +} + +export default function ChatbotFlowCanvas({ + nodes, + edges, + onNodesChange, + onEdgesChange, + onConnect, + onSelectNode, + updateNodeData, +}: ChatbotFlowCanvasProps) { + const contextValue = useMemo(() => ({ updateNodeData }), [updateNodeData]); + + return ( + + onSelectNode(node.id)} + fitView + fitViewOptions={{ maxZoom: 1, padding: 0.2 }} + proOptions={{ hideAttribution: true }} + > + + + + + ); +} diff --git a/app/components/chatbot/ChatbotFlowContext.tsx b/app/components/chatbot/ChatbotFlowContext.tsx new file mode 100644 index 00000000..9e49716a --- /dev/null +++ b/app/components/chatbot/ChatbotFlowContext.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { createContext, useContext } from "react"; +import { ChatbotNodeData } from "@/app/lib/types/chatbot"; + +interface ChatbotFlowContextValue { + updateNodeData: (id: string, patch: Partial) => void; +} + +export const ChatbotFlowContext = createContext( + null, +); + +/** Lets custom nodes edit their own data inline on the canvas. */ +export function useChatbotFlowContext(): ChatbotFlowContextValue { + const ctx = useContext(ChatbotFlowContext); + if (!ctx) { + throw new Error( + "useChatbotFlowContext must be used inside ChatbotFlowContext.Provider", + ); + } + return ctx; +} diff --git a/app/components/chatbot/ChatbotTestPanel.tsx b/app/components/chatbot/ChatbotTestPanel.tsx new file mode 100644 index 00000000..7884c61e --- /dev/null +++ b/app/components/chatbot/ChatbotTestPanel.tsx @@ -0,0 +1,119 @@ +/** + * ChatbotTestPanel - Runs a throwaway test conversation against the selected + * node's current (unsaved) prompt and config. + */ + +"use client"; + +import { FormEvent, useEffect, useRef, useState } from "react"; +import { Button } from "@/app/components/ui"; +import { MarkdownContent } from "@/app/components/chat"; +import { RefreshIcon, SendIcon } from "@/app/components/icons"; +import { useChatbotTest } from "@/app/hooks/useChatbotTest"; +import { ChatbotMessage, ChatbotNodeData } from "@/app/lib/types/chatbot"; + +interface ChatbotTestPanelProps { + data: ChatbotNodeData; +} + +function MessageBubble({ message }: { message: ChatbotMessage }) { + const isUser = message.role === "user"; + return ( +
+
+ {isUser ? message.content : } +
+
+ ); +} + +export default function ChatbotTestPanel({ data }: ChatbotTestPanelProps) { + const { history, pendingMessage, isPending, canSend, send, reset } = + useChatbotTest(data); + const [input, setInput] = useState(""); + const bottomRef = useRef(null); + + const visible = history.filter((m) => m.role !== "system"); + const hasStarted = history.length > 0; + + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "end" }); + }, [visible.length, pendingMessage, isPending]); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + const text = input.trim(); + if (!text || isPending) return; + setInput(""); + const ok = await send(text); + if (!ok) setInput(text); + }; + + return ( +
+
+ {!hasStarted && !isPending && ( +
+

+ {canSend + ? "Start a test - the bot will send the first message." + : "Pick a configuration and write a system prompt to start testing."} +

+ +
+ )} + {visible.map((m, i) => ( + + ))} + {pendingMessage && ( + + )} + {isPending && ( +

+ Bot is typing… +

+ )} +
+
+ + {hasStarted && ( +
+ + setInput(e.target.value)} + placeholder="Reply as the user…" + disabled={isPending} + className="flex-1 px-3 py-2 rounded-full border border-border text-sm bg-bg-primary text-text-primary focus:outline-none focus:border-accent-primary disabled:opacity-60" + /> + +
+ )} +
+ ); +} diff --git a/app/components/chatbot/LlmNode.tsx b/app/components/chatbot/LlmNode.tsx new file mode 100644 index 00000000..71c9c002 --- /dev/null +++ b/app/components/chatbot/LlmNode.tsx @@ -0,0 +1,88 @@ +/** + * LlmNode - Editable LLM step on the chatbot canvas: config, prompt and limits + * are edited inline, mirroring the node's data used by the test panel. + */ + +"use client"; + +import { Handle, NodeProps, Position } from "@xyflow/react"; +import { ChatConfigPicker } from "@/app/components/chat"; +import { ChatbotLlmNode, ChatbotNodeData } from "@/app/lib/types/chatbot"; +import { useChatbotFlowContext } from "@/app/components/chatbot/ChatbotFlowContext"; +import { FLOW_HANDLE_STYLE } from "@/app/components/chatbot/flowHandleStyle"; +import NodeFieldLabel from "@/app/components/chatbot/NodeFieldLabel"; +import LlmNodePromptField from "@/app/components/chatbot/LlmNodePromptField"; +import LlmNodeAdvanced from "@/app/components/chatbot/LlmNodeAdvanced"; + +const PORT_LABEL_CLASS = "text-sm font-semibold font-mono text-text-primary"; + +export default function LlmNode({ + id, + data, + selected, +}: NodeProps) { + const { updateNodeData } = useChatbotFlowContext(); + const update = (patch: Partial) => updateNodeData(id, patch); + + return ( +
+
+ update({ label: e.target.value })} + placeholder="Untitled step" + className="nodrag w-full px-6 text-center text-base font-semibold text-text-primary bg-transparent focus:outline-none" + /> +

{id}

+
+ +
+ + Input +
+ +
+
+ + + update({ configId, configVersion }) + } + alignLeft + /> +
+ + update({ systemPrompt })} + /> + + +
+ +
+ Output + +
+
+ ); +} diff --git a/app/components/chatbot/LlmNodeAdvanced.tsx b/app/components/chatbot/LlmNodeAdvanced.tsx new file mode 100644 index 00000000..726f43c9 --- /dev/null +++ b/app/components/chatbot/LlmNodeAdvanced.tsx @@ -0,0 +1,63 @@ +/** + * LlmNodeAdvanced - Collapsible per-request limits for an LLM node. + */ + +"use client"; + +import { useState } from "react"; +import { Field } from "@/app/components/ui"; +import { ChevronDownIcon, ChevronUpIcon } from "@/app/components/icons"; +import { ChatbotNodeData } from "@/app/lib/types/chatbot"; + +interface LlmNodeAdvancedProps { + data: ChatbotNodeData; + onChange: (patch: Partial) => void; +} + +function toPositiveInt(value: string, fallback: number): number { + const n = Number.parseInt(value, 10); + return Number.isFinite(n) && n > 0 ? n : fallback; +} + +export default function LlmNodeAdvanced({ + data, + onChange, +}: LlmNodeAdvancedProps) { + const [open, setOpen] = useState(false); + const Chevron = open ? ChevronUpIcon : ChevronDownIcon; + + return ( +
+ + {open && ( +
+ + onChange({ maxTokens: toPositiveInt(v, data.maxTokens) }) + } + /> + + onChange({ + historyTokenLimit: toPositiveInt(v, data.historyTokenLimit), + }) + } + /> +
+ )} +
+ ); +} diff --git a/app/components/chatbot/LlmNodePromptField.tsx b/app/components/chatbot/LlmNodePromptField.tsx new file mode 100644 index 00000000..2b3755da --- /dev/null +++ b/app/components/chatbot/LlmNodePromptField.tsx @@ -0,0 +1,73 @@ +/** + * LlmNodePromptField - Inline system-prompt editor with an expanded modal view. + */ + +"use client"; + +import { useState } from "react"; +import { createPortal } from "react-dom"; +import { Modal } from "@/app/components/ui"; +import { ExpandIcon } from "@/app/components/icons"; +import NodeFieldLabel from "@/app/components/chatbot/NodeFieldLabel"; + +interface LlmNodePromptFieldProps { + value: string; + onChange: (value: string) => void; +} + +const PLACEHOLDER = + "Goal: Register a student. Friendly, short WhatsApp messages, one question at a time."; + +export default function LlmNodePromptField({ + value, + onChange, +}: LlmNodePromptFieldProps) { + const [expanded, setExpanded] = useState(false); + + return ( +
+ + + +