From 6ce16e615a95c68809287fe932592cf184ef26b2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 17 Sep 2025 13:36:22 +0000 Subject: [PATCH 1/3] Revert "Seng/chat prototype (#279)" This reverts commit a61babf8e1ae1865cd5534780b8cbadfd07a0e56. --- .cursor/rules/overview.mdc | 6 +- .cursor/rules/tutorials-structure.mdc | 69 - .env.example | 7 +- .gitignore | 1 - agent-docs/agentuity.yaml | 3 - agent-docs/package.json | 2 +- agent-docs/src/agents/agent-pulse/README.md | 102 - .../src/agents/agent-pulse/context/builder.ts | 54 - agent-docs/src/agents/agent-pulse/index.ts | 143 - .../src/agents/agent-pulse/request/parser.ts | 49 - .../src/agents/agent-pulse/request/types.ts | 16 - agent-docs/src/agents/agent-pulse/state.ts | 46 - .../src/agents/agent-pulse/state/manager.ts | 54 - .../agents/agent-pulse/streaming/processor.ts | 115 - .../src/agents/agent-pulse/streaming/types.ts | 48 - agent-docs/src/agents/agent-pulse/tools.ts | 103 - agent-docs/src/agents/agent-pulse/tutorial.ts | 98 - agent-docs/src/agents/doc-qa/prompt.ts | 1 + agent-docs/src/agents/doc-qa/rag.ts | 7 +- app/(docs)/[[...slug]]/page.tsx | 2 - .../sessions/[sessionId]/messages/route.ts | 374 - app/api/sessions/[sessionId]/route.ts | 266 - app/api/sessions/route.ts | 149 - app/api/tutorials/[id]/route.ts | 43 - .../[id]/steps/[stepNumber]/route.ts | 178 - app/api/tutorials/route.ts | 77 - app/api/users/tutorial-state/route.ts | 119 - app/chat/SessionContext.tsx | 20 - app/chat/[sessionId]/page.tsx | 233 - app/chat/components/ChatInput.tsx | 74 - app/chat/components/ChatMessage.tsx | 155 - app/chat/components/ChatMessagesArea.tsx | 70 - app/chat/components/CodeBlock.tsx | 62 - app/chat/components/CodeEditor.tsx | 135 - app/chat/components/MarkdownRenderer.tsx | 85 - app/chat/components/SessionSidebar.tsx | 205 - .../components/SessionSidebarSkeleton.tsx | 100 - app/chat/components/TutorialFileChip.tsx | 33 - app/chat/layout.tsx | 114 - app/chat/page.tsx | 57 - app/chat/services/sessionService.ts | 296 - app/chat/types.ts | 101 - app/chat/utils/dateUtils.ts | 37 - app/chat/utils/useAutoResize.ts | 45 - app/chat/utils/useStreaming.ts | 209 - app/global.css | 61 - components/CodeFromFiles.tsx | 92 - components/DynamicIsland/CompactView.tsx | 62 - components/DynamicIsland/DynamicIsland.tsx | 111 - components/DynamicIsland/ExpandedView.tsx | 116 - components/DynamicIsland/IslandWrapper.tsx | 47 - components/DynamicIsland/README.md | 133 - components/DynamicIsland/index.ts | 23 - components/DynamicIsland/types.ts | 47 - components/DynamicIsland/useTutorial.ts | 134 - components/DynamicIsland/utils.ts | 114 - components/ui/skeleton.tsx | 16 - lib/config.ts | 18 - lib/env.ts | 80 +- lib/kv-store.ts | 289 - lib/tutorial/all-tutorials-reader.ts | 77 - lib/tutorial/index.ts | 3 - lib/tutorial/state-manager.ts | 121 - lib/tutorial/tutorial-reader.ts | 144 - lib/tutorial/types.ts | 23 - lib/validation/middleware.ts | 141 - middleware.ts | 22 +- package-lock.json | 8499 ++++++++++------- package.json | 21 +- 69 files changed, 4934 insertions(+), 9623 deletions(-) delete mode 100644 .cursor/rules/tutorials-structure.mdc delete mode 100644 agent-docs/src/agents/agent-pulse/README.md delete mode 100644 agent-docs/src/agents/agent-pulse/context/builder.ts delete mode 100644 agent-docs/src/agents/agent-pulse/index.ts delete mode 100644 agent-docs/src/agents/agent-pulse/request/parser.ts delete mode 100644 agent-docs/src/agents/agent-pulse/request/types.ts delete mode 100644 agent-docs/src/agents/agent-pulse/state.ts delete mode 100644 agent-docs/src/agents/agent-pulse/state/manager.ts delete mode 100644 agent-docs/src/agents/agent-pulse/streaming/processor.ts delete mode 100644 agent-docs/src/agents/agent-pulse/streaming/types.ts delete mode 100644 agent-docs/src/agents/agent-pulse/tools.ts delete mode 100644 agent-docs/src/agents/agent-pulse/tutorial.ts delete mode 100644 app/api/sessions/[sessionId]/messages/route.ts delete mode 100644 app/api/sessions/[sessionId]/route.ts delete mode 100644 app/api/sessions/route.ts delete mode 100644 app/api/tutorials/[id]/route.ts delete mode 100644 app/api/tutorials/[id]/steps/[stepNumber]/route.ts delete mode 100644 app/api/tutorials/route.ts delete mode 100644 app/api/users/tutorial-state/route.ts delete mode 100644 app/chat/SessionContext.tsx delete mode 100644 app/chat/[sessionId]/page.tsx delete mode 100644 app/chat/components/ChatInput.tsx delete mode 100644 app/chat/components/ChatMessage.tsx delete mode 100644 app/chat/components/ChatMessagesArea.tsx delete mode 100644 app/chat/components/CodeBlock.tsx delete mode 100644 app/chat/components/CodeEditor.tsx delete mode 100644 app/chat/components/MarkdownRenderer.tsx delete mode 100644 app/chat/components/SessionSidebar.tsx delete mode 100644 app/chat/components/SessionSidebarSkeleton.tsx delete mode 100644 app/chat/components/TutorialFileChip.tsx delete mode 100644 app/chat/layout.tsx delete mode 100644 app/chat/page.tsx delete mode 100644 app/chat/services/sessionService.ts delete mode 100644 app/chat/types.ts delete mode 100644 app/chat/utils/dateUtils.ts delete mode 100644 app/chat/utils/useAutoResize.ts delete mode 100644 app/chat/utils/useStreaming.ts delete mode 100644 components/CodeFromFiles.tsx delete mode 100644 components/DynamicIsland/CompactView.tsx delete mode 100644 components/DynamicIsland/DynamicIsland.tsx delete mode 100644 components/DynamicIsland/ExpandedView.tsx delete mode 100644 components/DynamicIsland/IslandWrapper.tsx delete mode 100644 components/DynamicIsland/README.md delete mode 100644 components/DynamicIsland/index.ts delete mode 100644 components/DynamicIsland/types.ts delete mode 100644 components/DynamicIsland/useTutorial.ts delete mode 100644 components/DynamicIsland/utils.ts delete mode 100644 components/ui/skeleton.tsx delete mode 100644 lib/config.ts delete mode 100644 lib/kv-store.ts delete mode 100644 lib/tutorial/all-tutorials-reader.ts delete mode 100644 lib/tutorial/index.ts delete mode 100644 lib/tutorial/state-manager.ts delete mode 100644 lib/tutorial/tutorial-reader.ts delete mode 100644 lib/tutorial/types.ts delete mode 100644 lib/validation/middleware.ts diff --git a/.cursor/rules/overview.mdc b/.cursor/rules/overview.mdc index 4a8925ac..51937d1b 100644 --- a/.cursor/rules/overview.mdc +++ b/.cursor/rules/overview.mdc @@ -10,8 +10,4 @@ This is the technical documentation for the Agentuity Cloud, which covers: - Examples, tutorials, samples (/content/Examples) - And the different SDKs (/content/SDKs) -This doc app is a NextJS app built on top of Fumadocs (https://fumadocs.vercel.app/docs/ui). - -This project also contains the agent the powers RAG and Tutorials. Those agents live in `/agent-docs` directory. -To run the agent server locally, go into that directory with `cd agent-docs` and then start the agent with `agentuity dev`. -To run the NextJS app, do `npm run dev` in the root of this repository. You will want to start these two apps in separate repositories. \ No newline at end of file +This doc app is a NextJS app built on top of Fumadocs (https://fumadocs.vercel.app/docs/ui). \ No newline at end of file diff --git a/.cursor/rules/tutorials-structure.mdc b/.cursor/rules/tutorials-structure.mdc deleted file mode 100644 index bf534346..00000000 --- a/.cursor/rules/tutorials-structure.mdc +++ /dev/null @@ -1,69 +0,0 @@ -### Rule: tutorials-structure - -Purpose: Define how tutorials are authored and rendered so docs and runnable code stay in sync. - -### Structure -- Narrative pages: `content/Tutorial//step-.mdx` -- Section navigation: `content/Tutorial/meta.json` (list MDX page slugs in order) -- Runnable example project: `examples//` (full project; exclude `node_modules/`, `.uv/`) -- Tutorial code snippets: must be imported from files via `` -- Other, non-project examples: use regular fenced code blocks in MDX - -### MDX authoring -- Each step MDX must use frontmatter: - - `title`, `description` (recommended: `stepNumber`, `next`, `prev`) -- Import real code from the example project using repo-root-relative paths via ``: - -```mdx ---- -title: Step 1 — Getting Started -description: Intro step ---- - - - - - - - - - -``` - -### `` component -- Available in MDX via the docs page components map -- Props: - - `snippets`: array of `{ path, lang?, from?, to?, title? }` - - `path`: repo-root-relative (must start with `/`), validated and read server-side - - `lang`: language for highlighting (auto-inferred if omitted) - - `from`, `to`: 1-based line range (inclusive) - - `title`: label per tab - - `title` (optional): heading displayed above the tabs -- Renders using shared `CodeBlock` styling and `Tabs` for multiple snippets - -### Example project conventions (`examples//`) -- Include: `package.json`, `tsconfig.json`, `src/**`, optional `README.md`, optional lockfile -- Exclude: `node_modules/` -- Optional hygiene: - - Add `examples/**` to `.eslintignore` if you don’t want repo-wide linting on example sources - - Add `examples/**` to root `tsconfig.json` `exclude` if you don’t want repo-wide type-checks -- Should be runnable in a sandbox (StackBlitz/CodeSandbox) via `package.json` scripts - -### Rendering pipeline -- Fumadocs loads MDX via `lib/source.ts` and `app/(docs)/[[...slug]]/page.tsx` -- `` reads files at build/server time and renders with `CodeBlock` - - -### Agent compatibility (optional) -- Step API: `GET /api/tutorials/:id/steps/:stepNumber` returns `{ mdx, snippets }`. -- The chat UI replaces each `` occurrence with one or more fenced code blocks by consuming entries from `snippets` in order. - -### Quality gates (recommended) -- CI verifies: - - All `content/Tutorial/**.mdx` pages referenced by `content/Tutorial/meta.json` build without errors - - All `` references resolve to existing files - - Optional: the example project type-checks (`tsc --noEmit`) or passes a smoke test - diff --git a/.env.example b/.env.example index fc86baf2..9f33510c 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,6 @@ # Agent Configuration AGENT_BASE_URL=http://127.0.0.1:3500 -AGENT_QA_ID=agent_9ccc5545e93644bd9d7954e632a55a61 -AGENT_PULSE_ID=agent_ddcb59aa4473f1323be5d9f5fb62b74e +AGENT_ID=agent_9ccc5545e93644bd9d7954e632a55a61 -# API key can be found in agent-docs .env AGENTUITY_SDK_KEY -AGENTUITY_API_KEY= \ No newline at end of file +# Alternative: You can also set the full URL instead of BASE_URL + ID +# AGENT_FULL_URL=http://127.0.0.1:3500/agent_9ccc5545e93644bd9d7954e632a55a61 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6c466111..38bf69a4 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,6 @@ yarn-error.log* .env*.local .env.local .env.production -.env .vercel next-env.d.ts .open-next diff --git a/agent-docs/agentuity.yaml b/agent-docs/agentuity.yaml index e4b99487..89d8f17b 100644 --- a/agent-docs/agentuity.yaml +++ b/agent-docs/agentuity.yaml @@ -78,6 +78,3 @@ agents: - id: agent_9ccc5545e93644bd9d7954e632a55a61 name: doc-qa description: Agent that can answer questions based on dev docs as the knowledge base - - id: agent_ddcb59aa4473f1323be5d9f5fb62b74e - name: agent-pulse - description: Agentuity web app agent that converses with users for generate conversation and structured docs tutorials. diff --git a/agent-docs/package.json b/agent-docs/package.json index 9bc7f850..b1e59c26 100644 --- a/agent-docs/package.json +++ b/agent-docs/package.json @@ -40,4 +40,4 @@ } }, "module": "index.ts" -} \ No newline at end of file +} diff --git a/agent-docs/src/agents/agent-pulse/README.md b/agent-docs/src/agents/agent-pulse/README.md deleted file mode 100644 index 8ff25f7c..00000000 --- a/agent-docs/src/agents/agent-pulse/README.md +++ /dev/null @@ -1,102 +0,0 @@ -# Pulse Agent - -A conversational AI agent for tutorial management built with OpenAI and structured responses. - -## Overview - -Pulse is a friendly AI assistant that helps users discover, start, and navigate through tutorials. It uses OpenAI's GPT-4o-mini with structured response generation to provide both conversational responses and actionable instructions. - -## Architecture - -### Core Components - -- **`index.ts`**: Main agent logic using `generateObject` for structured responses -- **`chat-helpers.ts`**: Conversation history management -- **`tutorial-helpers.ts`**: Tutorial content fetching and formatting -- **`tutorial.ts`**: Tutorial API integration - -### Response Structure - -The agent uses `generateObject` to return structured responses with two parts: - -```typescript -{ - message: string, // Conversational response for the user - actionable?: { // Optional action for the program to execute - type: 'start_tutorial' | 'next_step' | 'previous_step' | 'get_tutorials' | 'none', - tutorialId?: string, - step?: number - } -} -``` - -### How It Works - -1. **User Input**: Agent receives user message and conversation history -2. **LLM Processing**: OpenAI generates structured response with message and optional actionable object -3. **Action Execution**: Program intercepts actionable objects and executes them: - - `get_tutorials`: Fetches available tutorial list - - `start_tutorial`: Fetches real tutorial content from API - - `next_step`/`previous_step`: Navigate through tutorial steps (TODO) -4. **Response**: Returns conversational message plus any additional data (tutorial content, tutorial list, etc.) - -## Key Features - -- **Structured Responses**: Clean separation between conversation and actions -- **Real Tutorial Content**: No hallucinated content - all tutorial data comes from actual APIs -- **Context Awareness**: Maintains conversation history for natural references -- **Extensible Actions**: Easy to add new action types (next step, hints, etc.) -- **Debug Logging**: Comprehensive logging for troubleshooting - -## Example Interactions - -### Starting a Tutorial -**User**: "I want to learn the JavaScript SDK" - -**LLM Response**: -```json -{ - "message": "I'd be happy to help you start the JavaScript SDK tutorial!", - "actionable": { - "type": "start_tutorial", - "tutorialId": "javascript-sdk" - } -} -``` - -**Final Response**: -```json -{ - "response": "I'd be happy to help you start the JavaScript SDK tutorial!", - "tutorialData": { - "type": "tutorial_step", - "tutorialId": "javascript-sdk", - "tutorialTitle": "JavaScript SDK Tutorial", - "currentStep": 1, - "stepContent": "Welcome to the JavaScript SDK tutorial...", - "codeBlock": {...} - }, - "conversationHistory": [...] -} -``` - -### General Conversation -**User**: "What's the difference between TypeScript and JavaScript?" - -**LLM Response**: -```json -{ - "message": "TypeScript is a superset of JavaScript that adds static type checking...", - "actionable": { - "type": "none" - } -} -``` - -## Benefits - -- **Reliable**: No parsing or tool interception needed -- **Extensible**: Easy to add new action types -- **Clean**: Clear separation between conversation and actions -- **Debuggable**: Can see exactly what the LLM wants to do -- **No Hallucination**: Tutorial content comes from real APIs, not LLM generation diff --git a/agent-docs/src/agents/agent-pulse/context/builder.ts b/agent-docs/src/agents/agent-pulse/context/builder.ts deleted file mode 100644 index ca48a4f1..00000000 --- a/agent-docs/src/agents/agent-pulse/context/builder.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { AgentContext } from "@agentuity/sdk"; - -export async function buildSystemPrompt(tutorialContext: string, ctx: AgentContext): Promise { - try { - const systemPrompt = `=== ROLE === -You are Pulse, an AI assistant designed to help developers learn and navigate the Agentuity platform through interactive tutorials and clear guidance. Your primary goal is to assist users with understanding and using the Agentuity SDK effectively. When a user's query is vague, unclear, or lacks specific intent, subtly suggest relevant interactive tutorial to guide them toward learning the platform. For clear, specific questions related to the Agentuity SDK or other topics, provide direct, accurate, and concise answers without mentioning tutorials unless relevant. Always maintain a friendly and approachable tone to encourage engagement. - -Your role is to ensure user have a smooth tutorial experience! - -When user is asking to move to the next tutorial, simply increment the step for them. - -=== PERSONALITY === -- Friendly and encouraging with light humour -- Patient with learners at all levels -- Clear and concise in explanations -- Enthusiastic about teaching and problem-solving - -=== Available Tools or Functions === -You have access to various tools you can use -- use when appropriate! -1. Tutorial management - - startTutorialAtStep: Starting the user off at a specific step of a tutorial. -2. General assistance - - askDocsAgentTool: retrieve Agentuity documentation snippets - -=== TOOL-USAGE RULES (must follow) === -- startTutorialById must only be used when user select a tutorial. If the user starts a new tutorial, the step number should be set to one. Valid step is between 1 and totalSteps of the specific tutorial. -- Treat askDocsAgentTool as a search helper; ignore results you judge irrelevant. - -=== RESPONSE STYLE (format guidelines) === -- Begin with a short answer, then elaborate if necessary. -- Add brief comments to complex code; skip obvious lines. -- End with a question when further clarification could help the user. - -=== SAFETY & BOUNDARIES === -- If asked for private data or secrets, refuse. -- If the user requests actions outside your capabilities, apologise and explain. -- Keep every response < 400 words - -Generate a response to the user query accordingly and try to be helpful - -=== CONTEXT === -${tutorialContext} - -=== END OF PROMPT === - -Stream your reasoning steps clearly.`; - - ctx.logger.debug("Built system prompt with tutorial context"); - return systemPrompt; - } catch (error) { - ctx.logger.error("Failed to build system prompt: %s", error instanceof Error ? error.message : String(error)); - throw error; // Re-throw for centralized handling - } -} \ No newline at end of file diff --git a/agent-docs/src/agents/agent-pulse/index.ts b/agent-docs/src/agents/agent-pulse/index.ts deleted file mode 100644 index 5932c5fc..00000000 --- a/agent-docs/src/agents/agent-pulse/index.ts +++ /dev/null @@ -1,143 +0,0 @@ -import type { AgentRequest, AgentResponse, AgentContext } from "@agentuity/sdk"; -import { streamText } from "ai"; -import { openai } from "@ai-sdk/openai"; -import { createTools } from "./tools"; -import { createAgentState } from "./state"; -import { getTutorialList, type Tutorial } from "./tutorial"; -import { parseAgentRequest } from "./request/parser"; -import { buildSystemPrompt } from "./context/builder"; -import { createStreamingProcessor } from "./streaming/processor"; -import type { ConversationMessage, TutorialState } from "./request/types"; - -/** - * Builds a context string containing available tutorials for the system prompt - */ -async function buildContext( - ctx: AgentContext, - tutorialState?: TutorialState -): Promise { - try { - const tutorials = await getTutorialList(ctx); - - // Handle API failure early - if (!tutorials.success || !tutorials.data) { - ctx.logger.warn("Failed to load tutorial list"); - return defaultFallbackContext(); - } - - const tutorialContent = JSON.stringify(tutorials.data, null, 2); - const currentTutorialInfo = buildCurrentTutorialInfo( - tutorials.data, - tutorialState - ); - - return `===AVAILABLE TUTORIALS==== - - ${tutorialContent} - - ${currentTutorialInfo} - - Note: You should not expose the details of the tutorial IDs to the user. -`; - } catch (error) { - ctx.logger.error("Error building tutorial context: %s", error); - return defaultFallbackContext(); - } -} - -/** - * Builds current tutorial information string if user is in a tutorial - */ -function buildCurrentTutorialInfo( - tutorials: Tutorial[], - tutorialState?: TutorialState -): string { - if (!tutorialState?.tutorialId) { - return ""; - } - - const currentTutorial = tutorials.find( - (t) => t.id === tutorialState.tutorialId - ); - if (!currentTutorial) { - return "\nWarning: User appears to be in an unknown tutorial."; - } - if (tutorialState.currentStep > currentTutorial.totalSteps) { - return `\nUser has completed the tutorial: ${currentTutorial.title} (${currentTutorial.totalSteps} steps)`; - } - return `\nUser is currently on this tutorial: ${currentTutorial.title} (Step ${tutorialState.currentStep} of ${currentTutorial.totalSteps})`; -} - -/** - * Returns fallback context when tutorial list can't be loaded - */ -function defaultFallbackContext(): string { - return `===AVAILABLE TUTORIALS==== -Unable to load tutorial list. Please try again later or contact support.`; -} - -export default async function Agent( - req: AgentRequest, - resp: AgentResponse, - ctx: AgentContext -) { - try { - const parsedRequest = parseAgentRequest(await req.data.json(), ctx); - - // Create state manager - const state = createAgentState(); - - // Build messages for the conversation - const messages: ConversationMessage[] = [ - ...parsedRequest.conversationHistory, - { author: "USER", content: parsedRequest.message }, - ]; - - let tools: any; - let systemPrompt: string = ""; - // Direct LLM access won't require any tools or system prompt - if (!parsedRequest.useDirectLLM) { - // Create tools with state context - tools = await createTools({ - state, - agentContext: ctx, - }); - - // Build tutorial context and system prompt - const tutorialContext = await buildContext( - ctx, - parsedRequest.tutorialData - ); - systemPrompt = await buildSystemPrompt(tutorialContext, ctx); - } - - // Generate streaming response - const result = await streamText({ - model: openai("gpt-4o"), - messages: messages.map((msg) => ({ - role: msg.author === "USER" ? "user" : "assistant", - content: msg.content, - })), - tools, - maxSteps: 3, - system: systemPrompt, - }); - - // Create and return streaming response - const stream = createStreamingProcessor(result, state, ctx); - return resp.stream(stream, "text/event-stream"); - } catch (error) { - ctx.logger.error( - "Agent request failed: %s", - error instanceof Error ? error.message : String(error) - ); - return resp.json( - { - error: - "Sorry, I encountered an error while processing your request. Please try again.", - details: error instanceof Error ? error.message : String(error), - }, - { status: 500 } - ); - } -} diff --git a/agent-docs/src/agents/agent-pulse/request/parser.ts b/agent-docs/src/agents/agent-pulse/request/parser.ts deleted file mode 100644 index 91c5b254..00000000 --- a/agent-docs/src/agents/agent-pulse/request/parser.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { AgentContext } from "@agentuity/sdk"; -import type { ParsedAgentRequest } from "./types"; - -export function parseAgentRequest( - jsonData: any, - ctx: AgentContext -): ParsedAgentRequest { - try { - let message: string = ""; - let conversationHistory: any[] = []; - let tutorialData: any = undefined; - let useDirectLLM = false; - - if (jsonData && typeof jsonData === "object" && !Array.isArray(jsonData)) { - const body = jsonData as any; - message = body.message || ""; - useDirectLLM = body.use_direct_llm || false; - // Process conversation history - if (Array.isArray(body.conversationHistory)) { - conversationHistory = body.conversationHistory.map((msg: any) => { - // Extract only role and content - return { - role: msg.role || (msg.author ? msg.author.toUpperCase() : "USER"), - content: msg.content || "", - }; - }); - } - - tutorialData = body.tutorialData || undefined; - } else { - // Fallback for non-object data - message = String(jsonData || ""); - } - - return { - message, - conversationHistory, - tutorialData, - useDirectLLM, - }; - } catch (error) { - ctx.logger.error( - "Failed to parse agent request: %s", - error instanceof Error ? error.message : String(error) - ); - ctx.logger.debug("Raw request data: %s", JSON.stringify(jsonData)); - throw error; // Re-throw for centralized handling - } -} diff --git a/agent-docs/src/agents/agent-pulse/request/types.ts b/agent-docs/src/agents/agent-pulse/request/types.ts deleted file mode 100644 index 3f14e830..00000000 --- a/agent-docs/src/agents/agent-pulse/request/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -export interface ConversationMessage { - author: "USER" | "ASSISTANT"; - content: string; -} - -export interface TutorialState { - tutorialId: string; - currentStep: number; -} - -export interface ParsedAgentRequest { - message: string; - conversationHistory: ConversationMessage[]; - tutorialData?: TutorialState; - useDirectLLM?: boolean; -} \ No newline at end of file diff --git a/agent-docs/src/agents/agent-pulse/state.ts b/agent-docs/src/agents/agent-pulse/state.ts deleted file mode 100644 index 2cc4bc83..00000000 --- a/agent-docs/src/agents/agent-pulse/state.ts +++ /dev/null @@ -1,46 +0,0 @@ -enum ActionType { - START_TUTORIAL_STEP = "start_tutorial_step" -} - -interface Action { - type: ActionType; - tutorialId: string; - currentStep: number; - totalSteps: number; -} - -interface AgentState { - action: Action | null; - - setAction(action: Action): void; - getAction(): Action | null; - clearAction(): void; - hasAction(): boolean; -} - -class SimpleAgentState implements AgentState { - public action: Action | null = null; - - setAction(action: Action): void { - this.action = action; - } - - getAction(): Action | null { - return this.action; - } - - clearAction(): void { - this.action = null; - } - - hasAction(): boolean { - return this.action !== null; - } -} - -export function createAgentState(): AgentState { - return new SimpleAgentState(); -} - -export type { Action, AgentState }; -export { ActionType }; \ No newline at end of file diff --git a/agent-docs/src/agents/agent-pulse/state/manager.ts b/agent-docs/src/agents/agent-pulse/state/manager.ts deleted file mode 100644 index 00c1c570..00000000 --- a/agent-docs/src/agents/agent-pulse/state/manager.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { AgentContext } from "@agentuity/sdk"; -import { ActionType, type AgentState } from "../state"; -import { getTutorialStep } from "../tutorial"; -import type { TutorialData } from "../streaming/types"; - -export async function handleTutorialState( - state: AgentState, - ctx: AgentContext -): Promise { - try { - if (!state.hasAction()) { - return null; - } - - const action = state.getAction(); - if (!action) { - ctx.logger.warn("No action found in state"); - return null; - } - - ctx.logger.info("Processing action: %s", JSON.stringify(action, null, 2)); - - switch (action.type) { - case ActionType.START_TUTORIAL_STEP: - if (action.tutorialId) { - const tutorialStep = await getTutorialStep(action.tutorialId, action.currentStep, ctx); - if (tutorialStep.success && tutorialStep.data) { - const tutorialData: TutorialData = { - tutorialId: action.tutorialId, - totalSteps: action.totalSteps, - currentStep: action.currentStep, - tutorialStep: { - title: (tutorialStep.data.meta?.title as string) || tutorialStep.data.slug, - mdx: tutorialStep.data.mdx, - snippets: tutorialStep.data.snippets, - totalSteps: action.totalSteps - } - }; - state.clearAction(); - ctx.logger.info("Tutorial state processed successfully"); - return tutorialData; - } - } - break; - default: - ctx.logger.warn("Unknown action type: %s", action.type); - } - - return null; - } catch (error) { - ctx.logger.error("Failed to handle tutorial state: %s", error instanceof Error ? error.message : String(error)); - throw error; // Re-throw for centralized handling - } -} \ No newline at end of file diff --git a/agent-docs/src/agents/agent-pulse/streaming/processor.ts b/agent-docs/src/agents/agent-pulse/streaming/processor.ts deleted file mode 100644 index 72f89019..00000000 --- a/agent-docs/src/agents/agent-pulse/streaming/processor.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type { AgentContext } from "@agentuity/sdk"; -import type { AgentState } from "../state"; -import type { StreamingChunk, TutorialData } from "./types"; -import { handleTutorialState } from "../state/manager"; - -export function createStreamingProcessor( - result: any, - state: AgentState, - ctx: AgentContext -): ReadableStream { - return new ReadableStream({ - async start(controller) { - const encoder = new TextEncoder(); - let accumulatedContent = ""; - - try { - // Stream only safe, user-facing content - for await (const chunk of result.fullStream) { - await processChunk( - chunk, - controller, - encoder, - ctx, - accumulatedContent - ); - } - - // Process tutorial state after streaming text - const finalTutorialData = await handleTutorialState(state, ctx); - - // Send tutorial data if available - if (finalTutorialData) { - sendChunk(controller, encoder, { - type: "tutorial-data", - tutorialData: finalTutorialData, - }); - } - - // Send finish signal - sendChunk(controller, encoder, { type: "finish" }); - - controller.close(); - } catch (error) { - ctx.logger.error( - "Error in streaming response: %s", - error instanceof Error ? error.message : String(error) - ); - sendChunk(controller, encoder, { - type: "error", - error: "Sorry, I encountered an error while processing your request.", - details: error instanceof Error ? error.message : String(error), - }); - controller.close(); - } - }, - }); -} - -async function processChunk( - chunk: any, - controller: ReadableStreamDefaultController, - encoder: TextEncoder, - ctx: AgentContext, - accumulatedContent: string -): Promise { - try { - if (chunk.type === "text-delta") { - accumulatedContent += chunk.textDelta; - sendChunk(controller, encoder, { - type: "text-delta", - textDelta: chunk.textDelta, - }); - } else if (chunk.type === "tool-call") { - const toolName = chunk.toolName || "tool"; - const userFriendlyMessage = getToolStatusMessage(toolName); - sendChunk(controller, encoder, { - type: "status", - message: userFriendlyMessage, - category: "tool", - }); - ctx.logger.debug("Tool called: %s", toolName); - } else if (chunk.type === "reasoning") { - ctx.logger.debug("REASONING: %s", chunk); - } else { - ctx.logger.debug("Skipping chunk type: %s", chunk.type); - ctx.logger.debug(chunk); - } - } catch (error) { - ctx.logger.error( - "Failed to process chunk: %s", - error instanceof Error ? error.message : String(error) - ); - ctx.logger.debug("Chunk data: %s", JSON.stringify(chunk)); - throw error; // Re-throw for centralized handling - } -} - -function sendChunk( - controller: ReadableStreamDefaultController, - encoder: TextEncoder, - chunk: StreamingChunk -): void { - controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`)); -} - -function getToolStatusMessage(toolName: string): string { - switch (toolName) { - case "startTutorialById": - return "Starting tutorial..."; - case "queryOtherAgent": - return "Searching documentation..."; - default: - return "Processing your request..."; - } -} diff --git a/agent-docs/src/agents/agent-pulse/streaming/types.ts b/agent-docs/src/agents/agent-pulse/streaming/types.ts deleted file mode 100644 index 5fcaad52..00000000 --- a/agent-docs/src/agents/agent-pulse/streaming/types.ts +++ /dev/null @@ -1,48 +0,0 @@ -export interface TextDeltaChunk { - type: 'text-delta'; - textDelta: string; -} - -export interface StatusChunk { - type: 'status'; - message: string; - category?: 'tool' | 'search' | 'processing'; -} - -export interface TutorialSnippet { - path: string; - lang?: string; - from?: number; - to?: number; - title?: string; - content: string; -} - -export interface TutorialData { - tutorialId: string; - totalSteps: number; - currentStep: number; - tutorialStep: { - title: string; - mdx: string; - snippets: TutorialSnippet[]; - totalSteps: number; - }; -} - -export interface TutorialDataChunk { - type: 'tutorial-data'; - tutorialData: TutorialData; -} - -export interface ErrorChunk { - type: 'error'; - error: string; - details?: string; -} - -export interface FinishChunk { - type: 'finish'; -} - -export type StreamingChunk = TextDeltaChunk | StatusChunk | TutorialDataChunk | ErrorChunk | FinishChunk; \ No newline at end of file diff --git a/agent-docs/src/agents/agent-pulse/tools.ts b/agent-docs/src/agents/agent-pulse/tools.ts deleted file mode 100644 index a53b20d5..00000000 --- a/agent-docs/src/agents/agent-pulse/tools.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { tool } from "ai"; -import { z } from "zod"; -import { ActionType } from "./state"; -import type { AgentState } from "./state"; -import type { AgentContext } from "@agentuity/sdk"; -import { getTutorialMeta } from "./tutorial"; - -/** - * Context passed to tools for state management and logging - */ -interface ToolContext { - state: AgentState; - agentContext: AgentContext; -} - -/** - * Factory function that creates tools with state management context - */ -export async function createTools(context: ToolContext) { - const { state, agentContext } = context; - const DOC_QA_AGENT_NAME = "doc-qa"; - const docQaAgent = await agentContext.getAgent({ name: DOC_QA_AGENT_NAME }); - /** - * Tool for starting a tutorial - adds action to state queue - */ - const startTutorialAtStep = tool({ - description: "Start a specific tutorial for the user. You must call this function in order for the user to see the tutorial step content. The step number should be between 1 and the total number of steps in the tutorial.", - parameters: z.object({ - tutorialId: z.string().describe("The exact ID of the tutorial to start"), - stepNumber: z.number().describe("The step number of the tutorial to start (1 to total available steps in the tutorial)") - }), - execute: async ({ tutorialId, stepNumber }) => { - // Validate tutorial exists before starting - const tutorialResponse = await getTutorialMeta(tutorialId, agentContext); - if (!tutorialResponse.success || !tutorialResponse.data) { - return `Error fetching tutorial information`; - } - - const data = tutorialResponse.data - const totalSteps = tutorialResponse.data.totalSteps; - if (stepNumber > totalSteps) { - return `This tutorial only has ${totalSteps} steps. You either reached the end of the tutorial or selected an incorrect step number.`; - } - state.setAction({ - type: ActionType.START_TUTORIAL_STEP, - tutorialId: tutorialId, - currentStep: stepNumber, - totalSteps: tutorialResponse.data.totalSteps - }); - agentContext.logger.info("Added start_tutorial action to state for: %s at step %d", tutorialId, stepNumber); - return `Starting "${data.title}". Total steps: ${data.totalSteps} \n\n Description: ${data.description}`; - }, - }); - - /** - * Tool for talking to other agents (nong-tutorial functionality) - * This tool doesn't use state - it returns data directly - */ - const askDocsAgentTool = tool({ - description: "Query the Agentuity Development Documentation agent using RAG (Retrieval Augmented Generation) to get relevant documentation and answers about the Agentuity platform, APIs, and development concepts", - parameters: z.object({ - query: z.string().describe("The question or query to send to the query function"), - }), - execute: async ({ query }) => { - agentContext.logger.info("Querying agent %s with: %s", DOC_QA_AGENT_NAME, query); - const agentPayload = { - message: query, - - } - const response = await docQaAgent.run({ - data: agentPayload, - contentType: 'application/json' - }) - // TODO: handle the docs referencing and inject it to the frontend response - const responseData = await response.data.json(); - return responseData; - }, - }); - - /** - * TODO: This tool allow the agent to get details information about the code execution that the user performed. - */ - const fetchCodeExecutionResultTool = tool({ - description: "Fetch code execution results from the frontend", - parameters: z.object({ - executionId: z.string().describe("The ID of the code execution"), - }), - execute: async ({ executionId }) => { - agentContext.logger.info("Fetching execution result for: %s", executionId); - // This would actually fetch execution results - // For now, just return a mock response - return `Result for execution ${executionId}: console.log('Hello, World!');\n// Output: Hello, World!`; - }, - }); - - // Return tools object - return { - startTutorialById: startTutorialAtStep, - queryOtherAgent: askDocsAgentTool, - }; -} - -export type { ToolContext }; \ No newline at end of file diff --git a/agent-docs/src/agents/agent-pulse/tutorial.ts b/agent-docs/src/agents/agent-pulse/tutorial.ts deleted file mode 100644 index c5a00ed1..00000000 --- a/agent-docs/src/agents/agent-pulse/tutorial.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { AgentContext } from '@agentuity/sdk'; - -const TUTORIAL_API_BASE_URL = process.env.TUTORIAL_API_URL; - -export interface Tutorial { - id: string; - title: string; - description: string; - totalSteps: number; -} - -interface ApiResponse { - success: boolean; - data?: T; - error?: string; - status?: number; -} - -export interface TutorialSnippet { - path: string; - lang?: string; - from?: number; - to?: number; - title?: string; - content: string; -} - -interface TutorialStepResponseData { - tutorialId: string; - stepNumber: number; - slug: string; - meta: Record; - mdx: string; - snippets: TutorialSnippet[]; -} - -export async function getTutorialList(ctx: AgentContext): Promise> { - try { - const response = await fetch(`${TUTORIAL_API_BASE_URL}/api/tutorials`); - - if (!response.ok) { - const error = await response.json(); - ctx.logger.error('Tutorial API error: %s', error); - throw new Error(`Tutorial API error: ${JSON.stringify(error)}`); - } - - const tutorials = await response.json() as Tutorial[]; - ctx.logger.info('Fetched %d tutorials', tutorials.length); - - return { - success: true, - data: tutorials - }; - } catch (error) { - ctx.logger.error('Error fetching tutorial list: %s', error); - throw error; - } -} - -export async function getTutorialMeta(tutorialId: string, ctx: AgentContext): Promise> { - try { - // New behavior: fetch all, then find by id - const response = await fetch(`${TUTORIAL_API_BASE_URL}/api/tutorials`); - if (!response.ok) { - const error = `Failed to fetch tutorial list: ${response.statusText}`; - ctx.logger.error(error); - return { success: false, status: response.status, error: response.statusText }; - } - const tutorials = (await response.json()) as Tutorial[]; - const found = tutorials.find(t => t.id === tutorialId); - if (!found) { - return { success: false, status: 404, error: 'Tutorial not found' }; - } - return { success: true, data: found }; - } catch (error) { - ctx.logger.error('Error fetching tutorial metadata for %s: %s', tutorialId, error); - throw error; - } -} - -export async function getTutorialStep(tutorialId: string, stepNumber: number, ctx: AgentContext): Promise> { - try { - const response = await fetch(`${TUTORIAL_API_BASE_URL}/api/tutorials/${tutorialId}/steps/${stepNumber}`); - - if (!response.ok) { - ctx.logger.error('Failed to fetch tutorial step %d for tutorial %s: %s', stepNumber, tutorialId, response.statusText); - return { success: false, status: response.status, error: response.statusText }; - } - - const responseData = await response.json(); - ctx.logger.info('Fetched step %d for tutorial %s', stepNumber, tutorialId); - - return responseData as ApiResponse; - } catch (error) { - ctx.logger.error('Error fetching tutorial step %d for tutorial %s: %s', stepNumber, tutorialId, error); - throw error; - } -} \ No newline at end of file diff --git a/agent-docs/src/agents/doc-qa/prompt.ts b/agent-docs/src/agents/doc-qa/prompt.ts index 24aa5f01..cec823fc 100644 --- a/agent-docs/src/agents/doc-qa/prompt.ts +++ b/agent-docs/src/agents/doc-qa/prompt.ts @@ -55,6 +55,7 @@ Return ONLY the query text, nothing else.`; }); const rephrasedQuery = result.text?.trim() || input; + console.log(rephrasedQuery); // Log if we actually rephrased it if (rephrasedQuery !== input) { ctx.logger.info( diff --git a/agent-docs/src/agents/doc-qa/rag.ts b/agent-docs/src/agents/doc-qa/rag.ts index 5e13215e..3140fd26 100644 --- a/agent-docs/src/agents/doc-qa/rag.ts +++ b/agent-docs/src/agents/doc-qa/rag.ts @@ -25,7 +25,8 @@ Your role is to be as helpful as possible and try to assist user by answering th === RULES === 1. Use ONLY the content inside tags to craft your reply. If the required information is missing, state that the docs do not cover it. 2. Never fabricate or guess undocumented details. -3. Focus on answering the QUESTION with the available provided to you. Keep in mind some might not be relevant, so pick the ones that is relevant to the user's question. +3. Focus on answering the QUESTION with the available provided to you. Keep in mind some might not be relevant, + so pick the ones that is relevant to the user's question. 4. Ambiguity handling: • When contains more than one distinct workflow or context that could satisfy the question, do **not** choose for the user. • Briefly (≤ 2 sentences each) summarise each plausible interpretation and ask **one** clarifying question so the user can pick a path. @@ -40,8 +41,8 @@ Your role is to be as helpful as possible and try to assist user by answering th • Use **bold** for important terms and *italic* for emphasis when appropriate. • Use > blockquotes for important notes or warnings. 6. You may suggest concise follow-up questions or related topics that are present in . -7. If do not answer the question, state that explicitly and offer the closest documented topic; answer strictly from or ask one clarifying question if nothing related exists. -8. Keep a neutral, factual tone. +7. Keep a neutral, factual tone. + === OUTPUT FORMAT === Return **valid JSON only** matching this TypeScript type: diff --git a/app/(docs)/[[...slug]]/page.tsx b/app/(docs)/[[...slug]]/page.tsx index 653b12ba..1e04170f 100644 --- a/app/(docs)/[[...slug]]/page.tsx +++ b/app/(docs)/[[...slug]]/page.tsx @@ -19,7 +19,6 @@ import { source } from '@/lib/source'; import { CommunityButton } from '../../../components/Community'; import CopyPageDropdown from '../../../components/CopyPageDropdown'; import { NavButton } from '../../../components/NavButton'; -import CodeFromFiles from '../../../components/CodeFromFiles'; export default async function Page(props: { params: Promise<{ slug?: string[] }>; @@ -58,7 +57,6 @@ export default async function Page(props: { PopupTrigger, CodeExample, CLICommand, - CodeFromFiles, CommunityButton, Mermaid, NavButton, diff --git a/app/api/sessions/[sessionId]/messages/route.ts b/app/api/sessions/[sessionId]/messages/route.ts deleted file mode 100644 index 523b43d1..00000000 --- a/app/api/sessions/[sessionId]/messages/route.ts +++ /dev/null @@ -1,374 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getKVValue, setKVValue } from "@/lib/kv-store"; -import { - Session, - Message, - StreamingChunk, - TutorialData, -} from "@/app/chat/types"; -import { toISOString, getCurrentTimestamp } from "@/app/chat/utils/dateUtils"; -import { getAgentPulseConfig } from "@/lib/env"; -import { config } from "@/lib/config"; -import { parseAndValidateJSON, SessionMessageRequestSchema } from "@/lib/validation/middleware"; - -// Constants -const DEFAULT_CONVERSATION_HISTORY_LIMIT = 10; -const AGENT_REQUEST_TIMEOUT = 30000; // 30 seconds - - -function sanitizeTitle(input: string): string { - if (!input) return ''; - let s = input.trim(); - // Strip wrapping quotes/backticks - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith('\'') && s.endsWith('\'')) || (s.startsWith('`') && s.endsWith('`'))) { - s = s.slice(1, -1).trim(); - } - // Remove markdown emphasis - s = s.replace(/\*\*([^*]+)\*\*|\*([^*]+)\*|__([^_]+)__|_([^_]+)_/g, (_m, a, b, c, d) => a || b || c || d || ''); - // Remove emojis (basic unicode emoji ranges) - s = s.replace(/[\u{1F300}-\u{1FAFF}\u{1F900}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu, ''); - // Collapse whitespace - s = s.replace(/\s+/g, ' ').trim(); - // Sentence case - s = sentenceCase(s); - // Trim trailing punctuation noise - s = s.replace(/[\s\-–—:;,\.]+$/g, '').trim(); - // Enforce 60 chars - if (s.length > 60) s = s.slice(0, 60).trim(); - return s; -} - -function sentenceCase(str: string): string { - if (!str) return ''; - const lower = str.toLowerCase(); - return lower.charAt(0).toUpperCase() + lower.slice(1); -} - -/** - * POST /api/sessions/[sessionId]/messages - Add a message to a session and process with streaming - * - * This endpoint now handles: - * 1. Adding a user message to a session - * 2. Processing the message with the agent - * 3. Streaming the response back to the client - * 4. Saving the assistant's response when complete - */ -export async function POST( - request: NextRequest, - { params }: { params: Promise<{ sessionId: string }> } -) { - try { - const userId = request.cookies.get("chat_user_id")?.value; - if (!userId) { - return NextResponse.json({ error: "User ID not found" }, { status: 401 }); - } - - const paramsData = await params; - const sessionId = paramsData.sessionId; - - const validation = await parseAndValidateJSON(request, SessionMessageRequestSchema); - - if (!validation.success) { - return validation.response; - } - - const { message, processWithAgent } = validation.data; - - // Ensure timestamp is in ISO string format - if (message.timestamp) { - message.timestamp = toISOString(message.timestamp); - } - const sessionKey = `${userId}_${sessionId}`; - const sessionResponse = await getKVValue(sessionKey, { - storeName: config.defaultStoreName, - }); - - // Helper: background title generation and persistence - async function generateAndPersistTitle(sessionId: string, sessionKey: string, finalSession: Session) { - try { - if ((finalSession as any).title) { - return; // Title already set - } - // Build compact conversation history (last 10 messages, truncate content) - const HISTORY_LIMIT = 10; - const MAX_CONTENT_LEN = 400; - const history = finalSession.messages - .slice(-HISTORY_LIMIT) - .map(m => ({ - author: m.author, - content: (m.content || '').slice(0, MAX_CONTENT_LEN), - })); - - const prompt = `Generate a very short session title summarizing the conversation topic.\n\nRequirements:\n- sentence case\n- no emojis\n- <= 60 characters\n- no quotes or markdown\n- output the title only, no extra text`; - - const agentConfig = getAgentPulseConfig(); - const headers: Record = { 'Content-Type': 'application/json' }; - if (agentConfig.bearerToken) headers['Authorization'] = `Bearer ${agentConfig.bearerToken}`; - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 3000); - let agentResponse: Response | null = null; - try { - agentResponse = await fetch(agentConfig.url, { - method: 'POST', - headers, - body: JSON.stringify({ - message: prompt, - conversationHistory: history, - use_direct_llm: true, - }), - signal: controller.signal, - }); - } finally { - clearTimeout(timeoutId); - } - - if (!agentResponse || !agentResponse.ok) { - console.error(`[title-gen] failed: bad response ${agentResponse ? agentResponse.status : 'no-response'}`); - return; - } - - const reader = agentResponse.body?.getReader(); - if (!reader) { - console.error('[title-gen] failed: no response body'); - return; - } - - let accumulated = ''; - const textDecoder = new TextDecoder(); - while (true) { - const { done, value } = await reader.read(); - if (done) break; - if (value) { - const text = textDecoder.decode(value); - for (const line of text.split('\n')) { - if (line.startsWith('data: ')) { - try { - const ev = JSON.parse(line.slice(6)); - if (ev.type === 'text-delta' && ev.textDelta) accumulated += ev.textDelta; - if (ev.type === 'finish') { - try { await reader.cancel(); } catch { } - break; - } - } catch { } - } - } - } - } - - const candidate = sanitizeTitle(accumulated); - const title = candidate || 'New chat'; - - // Re-fetch and set title only if still empty - const latest = await getKVValue(sessionKey, { storeName: config.defaultStoreName }); - if (!latest.success || !latest.data) return; - const current = latest.data as any; - if (current.title) return; - current.title = title; - await setKVValue(sessionKey, current, { storeName: config.defaultStoreName }); - - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - if (msg.includes('The operation was aborted') || msg.includes('aborted')) { - console.error('[title-gen] timeout after 3000ms'); - } else { - console.error(`[title-gen] failed: ${msg}`); - } - } - } - if (!sessionResponse.success || !sessionResponse.data) { - return NextResponse.json({ error: "Session not found" }, { status: 404 }); - } - - const session = sessionResponse.data; - - const updatedSession: Session = { - ...session, - messages: [...session.messages, message], - }; - - try { - await setKVValue(sessionKey, updatedSession, { - storeName: config.defaultStoreName, - }); - } catch (error) { - console.error( - `Failed to save session after adding message. SessionId: ${sessionId}, Error details:`, - error instanceof Error ? error.message : String(error), - error instanceof Error && error.stack ? `Stack: ${error.stack}` : '' - ); - return NextResponse.json( - { - error: "Failed to save message to session", - details: "Unable to persist the message. Please try again." - }, - { status: 500 } - ); - } - - if (!processWithAgent || message.author !== "USER") { - return NextResponse.json( - { success: true, session: updatedSession }, - { status: 200 } - ); - } - - // Create assistant message placeholder for tracking - const assistantMessageId = crypto.randomUUID(); - - // Process with agent and stream response - const agentConfig = getAgentPulseConfig(); - const agentUrl = agentConfig.url; - - // Get current tutorial state for the user - const { TutorialStateManager } = await import('@/lib/tutorial/state-manager'); - const currentTutorialState = await TutorialStateManager.getCurrentTutorialState(userId); - - const agentPayload = { - message: message.content, - conversationHistory: updatedSession.messages.slice( - -DEFAULT_CONVERSATION_HISTORY_LIMIT - ), - tutorialData: currentTutorialState, - }; - - // Prepare headers with optional bearer token - const headers: Record = { - "Content-Type": "application/json", - }; - if (agentConfig.bearerToken) { - headers["Authorization"] = `Bearer ${agentConfig.bearerToken}`; - } - - // Real agent call (SSE response expected) - const agentResponse = await fetch(agentUrl, { - method: 'POST', - headers, - body: JSON.stringify(agentPayload), - signal: AbortSignal.timeout(AGENT_REQUEST_TIMEOUT), - }); - - if (!agentResponse.ok) { - throw new Error(`Agent responded with status: ${agentResponse.status}`); - } - - // Process streaming response - let accumulatedContent = ""; - let finalTutorialData: TutorialData | undefined = undefined; - - const transformStream = new TransformStream({ - async transform(chunk, controller) { - // Forward the chunk to the client - controller.enqueue(chunk); - - // Process the chunk to accumulate the full response - const text = new TextDecoder().decode(chunk); - const lines = text.split("\n"); - - for (const line of lines) { - if (line.startsWith("data: ")) { - try { - const data = JSON.parse(line.slice(6)) as StreamingChunk; - - if (data.type === "text-delta" && data.textDelta) { - accumulatedContent += data.textDelta; - } else if (data.type === "tutorial-data" && data.tutorialData) { - finalTutorialData = data.tutorialData; - - // Update user's tutorial progress - await TutorialStateManager.updateTutorialProgress( - userId, - finalTutorialData.tutorialId, - finalTutorialData.currentStep, - finalTutorialData.totalSteps - ); - } else if (data.type === "finish") { - // When the stream is finished, save the assistant message - const assistantMessage: Message = { - id: assistantMessageId, - author: "ASSISTANT", - content: accumulatedContent, - timestamp: getCurrentTimestamp(), - tutorialData: finalTutorialData, - }; - - const finalSession = { - ...updatedSession, - messages: [...updatedSession.messages, assistantMessage], - }; - - await setKVValue(sessionKey, finalSession, { - storeName: config.defaultStoreName, - }); - - // Trigger background title generation if missing - // Do not await to avoid delaying the client stream completion - void generateAndPersistTitle(sessionId, sessionKey, finalSession); - - // Send the final session in the finish event - controller.enqueue( - new TextEncoder().encode( - `data: ${JSON.stringify({ - type: "finish", - session: finalSession, - })}\n\n` - ) - ); - } - } catch (error) { - console.error("Error processing stream chunk:", error); - } - } - } - }, - }); - - // Pipe the agent response through our transform stream - const reader = agentResponse.body?.getReader(); - if (!reader) { - throw new Error("No response body from agent"); - } - const writer = transformStream.writable.getWriter(); - (async () => { - try { - while (true) { - const { done, value } = await reader.read(); - if (done) { - break; - } - try { - await writer.write(value); - } catch (writeError) { - console.error('Error writing to transform stream:', writeError); - throw writeError; - } - } - await writer.close(); - } catch (error) { - console.error('Error in stream processing:', error); - writer.abort(error); - } - })(); - - return new NextResponse(transformStream.readable, { - headers: { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - Connection: "keep-alive", - }, - }); - } catch (error) { - console.error("Error in messages API:", error); - // Log the full error stack trace for debugging - if (error instanceof Error) { - console.error("Error stack:", error.stack); - } - return new Response( - JSON.stringify({ - error: "Internal server error", - details: error instanceof Error ? error.message : String(error), - }), - { status: 500, headers: { "Content-Type": "application/json" } } - ); - } -} diff --git a/app/api/sessions/[sessionId]/route.ts b/app/api/sessions/[sessionId]/route.ts deleted file mode 100644 index a3900650..00000000 --- a/app/api/sessions/[sessionId]/route.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getKVValue, setKVValue, deleteKVValue } from '@/lib/kv-store'; -import { Session, Message, SessionSchema } from '@/app/chat/types'; -import { toISOString } from '@/app/chat/utils/dateUtils'; -import { config } from '@/lib/config'; -import { parseAndValidateJSON, SessionMessageOnlyRequestSchema } from '@/lib/validation/middleware'; - -/** - * GET /api/sessions/[sessionId] - Get a specific session - */ -export async function GET( - request: NextRequest, - { params }: { params: Promise<{ sessionId: string }> } -) { - try { - const userId = request.cookies.get('chat_user_id')?.value; - if (!userId) { - return NextResponse.json({ error: 'User ID not found' }, { status: 401 }); - } - - const paramsData = await params; - const sessionId = paramsData.sessionId; - const sessionKey = `${userId}_${sessionId}`; - const response = await getKVValue(sessionKey, { storeName: config.defaultStoreName }); - - if (!response.success) { - return NextResponse.json( - { error: response.error || 'Session not found' }, - { status: response.statusCode || 404 } - ); - } - - return NextResponse.json({ session: response.data }); - } catch (error) { - return NextResponse.json( - { error: error instanceof Error ? error.message : 'Unknown error occurred' }, - { status: 500 } - ); - } -} - -/** - * PUT /api/sessions/[sessionId] - Update a session - */ -export async function PUT( - request: NextRequest, - { params }: { params: Promise<{ sessionId: string }> } -) { - try { - const userId = request.cookies.get('chat_user_id')?.value; - if (!userId) { - return NextResponse.json({ error: 'User ID not found' }, { status: 401 }); - } - - const paramsData = await params; - const sessionId = paramsData.sessionId; - const sessionKey = `${userId}_${sessionId}`; - - const validation = await parseAndValidateJSON(request, SessionSchema); - if (!validation.success) { - return validation.response; - } - - const session = validation.data; - - if (session.sessionId !== sessionId) { - return NextResponse.json( - { error: 'Session ID mismatch' }, - { status: 400 } - ); - } - - // Process any messages to ensure timestamps are in ISO string format - if (session.messages && session.messages.length > 0) { - session.messages = session.messages.map((message: Message) => { - if (message.timestamp) { - return { - ...message, - timestamp: toISOString(message.timestamp) - }; - } - return message; - }); - } - - // Update the individual session - const response = await setKVValue( - sessionKey, - session, - { storeName: config.defaultStoreName } - ); - - if (!response.success) { - return NextResponse.json( - { error: response.error || 'Failed to update session' }, - { status: response.statusCode || 500 } - ); - } - - // Update the master list if needed (ensure the session ID is in the list) - const allSessionsResponse = await getKVValue(userId, { storeName: config.defaultStoreName }); - const sessionIds = allSessionsResponse.success ? allSessionsResponse.data || [] : []; - - // If the session ID isn't in the list, add it to the beginning - if (!sessionIds.includes(sessionKey)) { - const updatedSessionIds = [sessionKey, ...sessionIds]; - - const sessionsListResponse = await setKVValue( - userId, - updatedSessionIds, - { storeName: config.defaultStoreName } - ); - - if (!sessionsListResponse.success) { - // Log the error but don't fail the request - console.error('Failed to update sessions list:', sessionsListResponse.error); - } - } - - return NextResponse.json({ success: true, session }); - } catch (error) { - return NextResponse.json( - { error: error instanceof Error ? error.message : 'Unknown error occurred' }, - { status: 500 } - ); - } -} - -/** - * DELETE /api/sessions/[sessionId] - Delete a session - */ -export async function DELETE( - request: NextRequest, - { params }: { params: Promise<{ sessionId: string }> } -) { - try { - const userId = request.cookies.get('chat_user_id')?.value; - if (!userId) { - return NextResponse.json({ error: 'User ID not found' }, { status: 401 }); - } - - const paramsData = await params; - const sessionId = paramsData.sessionId; - const sessionKey = `${userId}_${sessionId}`; - // Delete the session data - const sessionResponse = await deleteKVValue( - sessionKey, - { storeName: config.defaultStoreName } - ); - - if (!sessionResponse.success) { - return NextResponse.json( - { error: sessionResponse.error || 'Failed to delete session' }, - { status: sessionResponse.statusCode || 500 } - ); - } - - // Remove from sessions list - const allSessionsResponse = await getKVValue(userId, { storeName: config.defaultStoreName }); - const sessionIds = allSessionsResponse.success ? allSessionsResponse.data || [] : []; - - const updatedSessionIds = sessionIds.filter(id => id !== sessionKey); - - const sessionsListResponse = await setKVValue( - userId, - updatedSessionIds, - { storeName: config.defaultStoreName } - ); - - if (!sessionsListResponse.success) { - return NextResponse.json( - { error: sessionsListResponse.error || 'Failed to update sessions list' }, - { status: sessionsListResponse.statusCode || 500 } - ); - } - - return NextResponse.json({ success: true }); - } catch (error) { - return NextResponse.json( - { error: error instanceof Error ? error.message : 'Unknown error occurred' }, - { status: 500 } - ); - } -} - -/** - * POST /api/sessions/[sessionId]/messages - Add a message to a session - */ -export async function POST( - request: NextRequest, - { params }: { params: Promise<{ sessionId: string }> } -) { - try { - const userId = request.cookies.get('chat_user_id')?.value; - if (!userId) { - return NextResponse.json({ error: 'User ID not found' }, { status: 401 }); - } - - const paramsData = await params; - const sessionId = paramsData.sessionId; - const sessionKey = `${userId}_${sessionId}`; - - const validation = await parseAndValidateJSON(request, SessionMessageOnlyRequestSchema); - - if (!validation.success) { - return validation.response; - } - - const { message } = validation.data; - - // Get current session - const sessionResponse = await getKVValue(sessionKey, { storeName: config.defaultStoreName }); - if (!sessionResponse.success || !sessionResponse.data) { - return NextResponse.json( - { error: 'Session not found' }, - { status: 404 } - ); - } - - const session = sessionResponse.data; - const updatedSession: Session = { - ...session, - messages: [...session.messages, message] - }; - - // Update the individual session - const updateResponse = await setKVValue( - sessionKey, - updatedSession, - { storeName: config.defaultStoreName } - ); - - if (!updateResponse.success) { - return NextResponse.json( - { error: updateResponse.error || 'Failed to update session' }, - { status: updateResponse.statusCode || 500 } - ); - } - - // Move this session ID to the top of the master list (most recently used) - const allSessionsResponse = await getKVValue(userId, { storeName: config.defaultStoreName }); - const sessionIds = allSessionsResponse.success ? allSessionsResponse.data || [] : []; - - // Remove the current session ID if it exists and add it to the beginning - const filteredSessionIds = sessionIds.filter(id => id !== sessionKey); - const updatedSessionIds = [sessionKey, ...filteredSessionIds]; - - const sessionsListResponse = await setKVValue( - userId, - updatedSessionIds, - { storeName: config.defaultStoreName } - ); - - if (!sessionsListResponse.success) { - // Log the error but don't fail the request since we already updated the individual session - console.error('Failed to update sessions list:', sessionsListResponse.error); - } - - return NextResponse.json({ success: true, session: updatedSession }); - } catch (error) { - return NextResponse.json( - { error: error instanceof Error ? error.message : 'Unknown error occurred' }, - { status: 500 } - ); - } -} diff --git a/app/api/sessions/route.ts b/app/api/sessions/route.ts deleted file mode 100644 index bd6a97f2..00000000 --- a/app/api/sessions/route.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getKVValue, setKVValue } from '@/lib/kv-store'; -import { Session, Message, SessionSchema } from '@/app/chat/types'; -import { toISOString } from '@/app/chat/utils/dateUtils'; -import { config } from '@/lib/config'; -import { parseAndValidateJSON } from '@/lib/validation/middleware'; - -// Constants -const DEFAULT_SESSIONS_LIMIT = 10; -const MAX_SESSIONS_LIMIT = 50; - -/** - * GET /api/sessions - Get all sessions (paginated) - */ -export async function GET(request: NextRequest) { - try { - const userId = request.cookies.get('chat_user_id')?.value; - if (!userId) { - return NextResponse.json({ error: 'User ID not found' }, { status: 401 }); - } - - const searchParams = request.nextUrl.searchParams; - const parsedLimit = Number.parseInt(searchParams.get('limit') ?? String(DEFAULT_SESSIONS_LIMIT)); - const parsedCursor = Number.parseInt(searchParams.get('cursor') ?? '0'); - - const limit = Number.isFinite(parsedLimit) ? Math.min(Math.max(parsedLimit, 1), MAX_SESSIONS_LIMIT) : DEFAULT_SESSIONS_LIMIT; - const cursor = Number.isFinite(parsedCursor) ? Math.max(parsedCursor, 0) : 0; - - const response = await getKVValue(userId, { storeName: config.defaultStoreName }); - if (!response.success) { - if (response.statusCode === 404) { - return NextResponse.json({ sessions: [], pagination: { cursor, nextCursor: null, hasMore: false, total: 0, limit } }); - } - return NextResponse.json( - { error: response.error || 'Failed to retrieve sessions' }, - { status: response.statusCode || 500 } - ); - } - - if (!response.data?.length) { - return NextResponse.json({ sessions: [], pagination: { cursor, nextCursor: null, hasMore: false, total: 0, limit } }); - } - - const sessionIds = response.data; - const total = sessionIds.length; - - const start = Math.min(cursor, total); - const end = Math.min(start + limit, total); - const pageIds = sessionIds.slice(start, end); - - const sessionPromises = pageIds.map(sessionId => getKVValue(sessionId, { storeName: config.defaultStoreName })); - const sessionResults = await Promise.all(sessionPromises); - const sessions = sessionResults - .filter(result => result.success && result.data) - .map(result => result.data as Session); - - const hasMore = end < total; - const nextCursor = hasMore ? end : null; - - return NextResponse.json({ - sessions, - pagination: { cursor: start, nextCursor, hasMore, total, limit } - }); - } catch (error) { - return NextResponse.json( - { error: error instanceof Error ? error.message : 'Unknown error occurred' }, - { status: 500 } - ); - } -} - -/** - * POST /api/sessions - Create a new session - */ -export async function POST(request: NextRequest) { - try { - const userId = request.cookies.get('chat_user_id')?.value; - if (!userId) { - return NextResponse.json({ error: 'User ID not found' }, { status: 401 }); - } - - const validation = await parseAndValidateJSON(request, SessionSchema); - if (!validation.success) { - return validation.response; - } - - const session = validation.data; - - // Process any messages to ensure timestamps are in ISO string format - if (session.messages && session.messages.length > 0) { - session.messages = session.messages.map((message: Message) => { - if (message.timestamp) { - return { - ...message, - timestamp: toISOString(message.timestamp) - }; - } - return message; - }); - } - - const sessionKey = `${userId}_${session.sessionId}`; - - // Save the session data - const sessionResponse = await setKVValue( - sessionKey, - session, - { storeName: config.defaultStoreName } - ); - - if (!sessionResponse.success) { - return NextResponse.json( - { error: sessionResponse.error || 'Failed to create session' }, - { status: sessionResponse.statusCode || 500 } - ); - } - - // Update the sessions list with just the session ID - const allSessionsResponse = await getKVValue(userId, { storeName: config.defaultStoreName }); - const sessionIds = allSessionsResponse.success ? allSessionsResponse.data || [] : []; - - // Add the new session ID to the beginning of the array - const updatedSessionIds = [sessionKey, ...sessionIds.filter(id => id !== sessionKey)]; - - const sessionsListResponse = await setKVValue( - userId, - updatedSessionIds, - { storeName: config.defaultStoreName } - ); - - if (!sessionsListResponse.success) { - return NextResponse.json( - { error: sessionsListResponse.error || 'Failed to update sessions list' }, - { status: sessionsListResponse.statusCode || 500 } - ); - } - - return NextResponse.json({ - success: true, - session, - ...(session.title ? {} : { titleGeneration: 'pending' }) - }); - } catch (error) { - return NextResponse.json( - { error: error instanceof Error ? error.message : 'Unknown error occurred' }, - { status: 500 } - ); - } -} diff --git a/app/api/tutorials/[id]/route.ts b/app/api/tutorials/[id]/route.ts deleted file mode 100644 index 637fb1a3..00000000 --- a/app/api/tutorials/[id]/route.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { readAllTutorials } from '@/lib/tutorial/all-tutorials-reader'; -import { resolve } from 'path'; - -interface RouteParams { - params: Promise<{ id: string }>; -} - -export async function GET(request: NextRequest, { params }: RouteParams) { - try { - const { id } = await params; - const basePath = resolve(process.cwd()); - - // Read all tutorials and find the one with matching ID from meta.json - const allTutorials = await readAllTutorials(basePath); - const tutorial = allTutorials.find(t => t.id === id); - - if (!tutorial) { - return NextResponse.json( - { - success: false, - error: 'Tutorial not found' - }, - { status: 404 } - ); - } - - return NextResponse.json({ - success: true, - data: tutorial - }); - } catch (error) { - console.error('Error reading tutorial:', error); - return NextResponse.json( - { - success: false, - error: 'Failed to read tutorial', - message: error instanceof Error ? error.message : String(error) - }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/api/tutorials/[id]/steps/[stepNumber]/route.ts b/app/api/tutorials/[id]/steps/[stepNumber]/route.ts deleted file mode 100644 index e949733d..00000000 --- a/app/api/tutorials/[id]/steps/[stepNumber]/route.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { join, resolve, sep } from 'path'; -import { readFile } from 'fs/promises'; -import matter from 'gray-matter'; -import { validateTutorialId, validateStepNumber, createValidationError } from '@/lib/validation/middleware'; - -interface RouteParams { - params: Promise<{ id: string; stepNumber: string }>; -} - -export async function GET(request: NextRequest, { params }: RouteParams) { - try { - const { id, stepNumber } = await params; - - const idValidation = validateTutorialId(id); - if (!idValidation.success) { - return createValidationError('Invalid tutorial ID', idValidation.errors || []); - } - - const stepValidation = validateStepNumber(stepNumber); - if (!stepValidation.success) { - return createValidationError('Invalid step number', stepValidation.errors || []); - } - - const stepIndex = stepValidation.data; - if (!stepIndex) { - return NextResponse.json( - { success: false, error: 'Invalid step number' }, - { status: 400 } - ); - } - - const repoRoot = process.cwd(); - const tutorialDir = join(repoRoot, 'content', 'Tutorial', id); - - // Load child tutorial meta.json to get ordered pages - const childMetaRaw = await readFile(join(tutorialDir, 'meta.json'), 'utf-8'); - const childMeta = JSON.parse(childMetaRaw) as { title?: string; pages?: string[] }; - const pages = (childMeta.pages ?? []).filter(Boolean); - - // Filter out index; map to actual MDX files - const stepSlugs = pages.filter(p => p !== 'index'); - - if (stepIndex < 1 || stepIndex > stepSlugs.length) { - return NextResponse.json( - { success: false, error: 'Step not found' }, - { status: 404 } - ); - } - - const slug = stepSlugs[stepIndex - 1]; - if (!slug) { - return NextResponse.json( - { success: false, error: 'Step not found' }, - { status: 404 } - ); - } - - const mdxPath = join(tutorialDir, `${slug}.mdx`); - const mdxRaw = await readFile(mdxPath, 'utf-8'); - const parsed = matter(mdxRaw); - - // Extract CodeFromFiles tags and resolve their snippet arrays - const snippets: Array<{ path: string; lang?: string; from?: number; to?: number; title?: string; content: string }> = []; - - // Helper to load a snippet descriptor into content - async function loadSnippet(desc: { path: string; lang?: string; from?: number; to?: number; title?: string }) { - const filePath = desc.path; - if (!filePath || !filePath.startsWith('/examples/')) return; - - // Resolve against repo root and ensure containment within /examples - const resolvedPath = resolve(repoRoot, `.${filePath}`); - const examplesBase = resolve(repoRoot, 'examples'); - const isContained = resolvedPath === examplesBase || resolvedPath.startsWith(examplesBase + sep); - if (!isContained) return; - - try { - const fileRaw = await readFile(resolvedPath, 'utf-8'); - const lines = fileRaw.split(/\r?\n/); - const startIdx = Math.max(0, (desc.from ? desc.from - 1 : 0)); - const endIdx = Math.min(lines.length, desc.to ? desc.to : lines.length); - const content = lines.slice(startIdx, endIdx).join('\n'); - snippets.push({ ...desc, content }); - } catch (error) { - console.warn(`Failed to load snippet from ${filePath}:`, error); - } - } - - // 1) Parse blocks - // Robust parser that balances braces to extract snippets={[ ... ]} - const filesTagRegex = /]*?)\/>/g; - let filesTagMatch: RegExpExecArray | null; - while ((filesTagMatch = filesTagRegex.exec(parsed.content)) !== null) { - const propsSrc: string = filesTagMatch[1] || ''; - - const key = 'snippets={'; - const start = propsSrc.indexOf(key); - if (start < 0) continue; - let i = start + key.length; // position after '{' - let depth = 1; - // Scan until matching closing '}' for the snippets prop - while (i < propsSrc.length && depth > 0) { - const ch = propsSrc[i]; - if (ch === '{') depth++; - else if (ch === '}') depth--; - i++; - } - // slice without outer braces - const inner = propsSrc.slice(start + key.length, i - 1).trim(); // should be array source like "[{...},{...}]" - - // Extract object literals from the array by balancing braces again - const objects: string[] = []; - let j = 0; - while (j < inner.length) { - if (inner[j] === '{') { - let d = 1; - let k = j + 1; - while (k < inner.length && d > 0) { - const ch = inner[k]; - if (ch === '{') d++; - else if (ch === '}') d--; - k++; - } - objects.push(inner.slice(j, k)); - j = k; - } else { - j++; - } - } - - for (const objSrc of objects) { - const getStr = (name: string): string | undefined => { - const r = new RegExp(name + '\\s*:\\s*"([^"]*)"'); - const mm = r.exec(objSrc); - return mm ? mm[1] : undefined; - }; - const getNum = (name: string): number | undefined => { - const r = new RegExp(name + '\\s*:\\s*(\\d+)'); - const mm = r.exec(objSrc); - return mm ? Number(mm[1]) : undefined; - }; - const desc = { - path: getStr('path') || '', - lang: getStr('lang'), - from: getNum('from'), - to: getNum('to'), - title: getStr('title') - }; - if (desc.path) { - await loadSnippet(desc); - } - } - } - - return NextResponse.json({ - success: true, - data: { - tutorialId: id, - stepNumber: stepIndex, - slug, - meta: parsed.data ?? {}, - mdx: parsed.content, - snippets, - totalSteps: pages.length - } - }); - } catch (error) { - console.error('Error reading tutorial step:', error); - return NextResponse.json( - { - success: false, - error: 'Failed to read tutorial step', - message: error instanceof Error ? error.message : String(error) - }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/api/tutorials/route.ts b/app/api/tutorials/route.ts deleted file mode 100644 index be81bb7c..00000000 --- a/app/api/tutorials/route.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { join } from 'path'; -import { readFile, readdir, stat } from 'fs/promises'; -import matter from 'gray-matter'; - -export async function GET(request: NextRequest) { - try { - const repoRoot = process.cwd(); - const tutorialRoot = join(repoRoot, 'content', 'Tutorial'); - - // Use parent meta.json to control order and which tutorials show up - const parentMetaPath = join(tutorialRoot, 'meta.json'); - const parentMetaRaw = await readFile(parentMetaPath, 'utf-8'); - const parentMeta = JSON.parse(parentMetaRaw) as { title?: string; pages?: string[] }; - const pages = (parentMeta.pages ?? []).filter(Boolean); - - const results: Array<{ id: string; title: string; description?: string; totalSteps: number }> = []; - - for (const entry of pages) { - if (entry.includes('..') || entry.includes('/') || entry.includes('\\')) { - continue; - } - - const dirPath = join(tutorialRoot, entry); - const filePath = join(tutorialRoot, `${entry}.mdx`); - try { - const st = await stat(dirPath).catch(() => null); - if (st && st.isDirectory()) { - // Directory tutorial: read its meta.json and index.mdx (if present) - const childMetaRaw = await readFile(join(dirPath, 'meta.json'), 'utf-8'); - const childMeta = JSON.parse(childMetaRaw) as { title?: string; pages?: string[] }; - - const pagesList = childMeta.pages ?? []; - const totalSteps = pagesList.filter(p => p !== 'index').length || 0; - - let description: string | undefined; - try { - const idxPath = join(dirPath, 'index.mdx'); - const idxRaw = await readFile(idxPath, 'utf-8'); - const idx = matter(idxRaw); - if (typeof idx.data?.description === 'string') description = idx.data.description; - } catch { - // ignore if index missing - } - - results.push({ - id: entry, - title: childMeta.title || entry, - description, - totalSteps - }); - } else { - // Single-file tutorial - const mdxRaw = await readFile(filePath, 'utf-8'); - const fm = matter(mdxRaw); - const title = (fm.data?.title as string) || entry; - const description = typeof fm.data?.description === 'string' ? (fm.data.description as string) : undefined; - results.push({ id: entry, title, description, totalSteps: 1 }); - } - } catch (err) { - // Skip malformed entries but continue - // eslint-disable-next-line no-console - console.warn(`Skipping tutorial entry ${entry}:`, err); - } - } - - return NextResponse.json(results); - } catch (error) { - console.error('Error reading tutorials:', error); - - // Return proper HTTP error status with minimal error info - return NextResponse.json( - { error: 'Failed to read tutorials' }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/api/users/tutorial-state/route.ts b/app/api/users/tutorial-state/route.ts deleted file mode 100644 index 70557fa7..00000000 --- a/app/api/users/tutorial-state/route.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { TutorialStateManager } from '@/lib/tutorial/state-manager'; -import { setKVValue } from '@/lib/kv-store'; -import { config } from '@/lib/config'; -import { - parseAndValidateJSON, - TutorialProgressRequestSchema, - TutorialResetRequestSchema -} from '@/lib/validation/middleware'; - -/** - * GET /api/users/tutorial-state - Get current user's tutorial state - */ -export async function GET(request: NextRequest) { - try { - const userId = request.cookies.get('chat_user_id')?.value; - if (!userId) { - return NextResponse.json({ error: 'User ID not found' }, { status: 401 }); - } - - const tutorialState = await TutorialStateManager.getUserTutorialState(userId); - - return NextResponse.json({ - success: true, - data: tutorialState - }); - } catch (error) { - console.error('Error getting tutorial state:', error); - return NextResponse.json( - { error: 'Failed to get tutorial state' }, - { status: 500 } - ); - } -} - -/** - * POST /api/users/tutorial-state - Update tutorial progress - */ -export async function POST(request: NextRequest) { - try { - const userId = request.cookies.get('chat_user_id')?.value; - if (!userId) { - return NextResponse.json({ error: 'User ID not found' }, { status: 401 }); - } - - const validation = await parseAndValidateJSON(request, TutorialProgressRequestSchema); - if (!validation.success) { - return validation.response; - } - - const { tutorialId, currentStep, totalSteps } = validation.data; - - await TutorialStateManager.updateTutorialProgress( - userId, - tutorialId, - currentStep, - totalSteps - ); - - return NextResponse.json({ - success: true, - message: 'Tutorial progress updated' - }); - } catch (error) { - console.error('Error updating tutorial state:', error); - return NextResponse.json( - { error: 'Failed to update tutorial state' }, - { status: 500 } - ); - } -} - -/** - * DELETE /api/users/tutorial-state - Reset tutorial progress - */ -export async function DELETE(request: NextRequest) { - try { - const userId = request.cookies.get('chat_user_id')?.value; - if (!userId) { - return NextResponse.json({ error: 'User ID not found' }, { status: 401 }); - } - - const validation = await parseAndValidateJSON(request, TutorialResetRequestSchema); - if (!validation.success) { - return validation.response; - } - - const { tutorialId } = validation.data; - - const state = await TutorialStateManager.getUserTutorialState(userId); - if (!state.tutorials) { - state.tutorials = {}; - } - delete state.tutorials[tutorialId]; - - // Save the updated state - const kvResponse = await setKVValue(`tutorial_state_${userId}`, state, { - storeName: config.defaultStoreName - }); - - if (!kvResponse.success) { - return NextResponse.json( - { error: kvResponse.error || 'Failed to reset tutorial state' }, - { status: kvResponse.statusCode || 500 } - ); - } - - return NextResponse.json({ - success: true, - message: 'Tutorial progress reset' - }); - } catch (error) { - console.error('Error resetting tutorial state:', error); - return NextResponse.json( - { error: 'Failed to reset tutorial state' }, - { status: 500 } - ); - } -} diff --git a/app/chat/SessionContext.tsx b/app/chat/SessionContext.tsx deleted file mode 100644 index 03e36b79..00000000 --- a/app/chat/SessionContext.tsx +++ /dev/null @@ -1,20 +0,0 @@ - import { createContext, useContext } from 'react'; - import { Session } from './types'; - - interface SessionContextType { - sessions: Session[]; - setSessions: (updater: React.SetStateAction, options?: { revalidate: boolean }) => void; - currentSessionId: string; - // A simple trigger to revalidate sessions; implementation may vary under the hood - revalidateSessions?: () => void | Promise; - } - -export const SessionContext = createContext(undefined); - -export const useSessions = () => { - const context = useContext(SessionContext); - if (!context) { - throw new Error('useSessions must be used within a SessionProvider'); - } - return context; -}; diff --git a/app/chat/[sessionId]/page.tsx b/app/chat/[sessionId]/page.tsx deleted file mode 100644 index ef569ef4..00000000 --- a/app/chat/[sessionId]/page.tsx +++ /dev/null @@ -1,233 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; -import { useParams } from "next/navigation"; -import { Allotment } from "allotment"; -import "allotment/dist/style.css"; -import { v4 as uuidv4 } from 'uuid'; -import { ChatMessagesArea } from '../components/ChatMessagesArea'; -import { CodeEditor } from '../components/CodeEditor'; -import { Session, Message } from '../types'; -import { useSessions } from '../SessionContext'; -import { sessionService } from '../services/sessionService'; -import { Skeleton } from '@/components/ui/skeleton'; - -export default function ChatSessionPage() { - const { sessionId } = useParams<{ sessionId: string }>(); - const [session, setSession] = useState(); - const [editorOpen, setEditorOpen] = useState(false); - const [editorContent, setEditorContent] = useState(''); - const { sessions, setSessions, revalidateSessions } = useSessions(); - const [creationError, setCreationError] = useState(null); - - - const handleSendMessage = async (content: string, sessionId: string) => { - if (!content || !sessionId) return; - - const newMessage: Message = { - id: uuidv4(), - author: 'USER', - content: content, - timestamp: new Date().toISOString() - }; - - const assistantMessage: Message = { - id: uuidv4(), - author: 'ASSISTANT', - content: '', - timestamp: new Date().toISOString() - }; - - try { - setSession(prevSession => { - if (!prevSession) return prevSession; - return { - ...prevSession, - messages: [...prevSession.messages, newMessage, assistantMessage] - }; - }); - - await sessionService.addMessageToSessionStreaming( - sessionId, - newMessage, - { - onTextDelta: (textDelta) => { - setSession(prev => { - if (!prev) return prev; - const updatedMessages = prev.messages.map(msg => { - if (msg.id === assistantMessage.id) { - return { - ...msg, - content: msg.content + textDelta - }; - } - return msg; - }); - return { ...prev, messages: updatedMessages }; - }); - }, - - onTutorialData: (tutorialData) => { - setSession(prev => { - if (!prev) return prev; - const updatedMessages = prev.messages.map(msg => - msg.id === assistantMessage.id - ? { ...msg, tutorialData: tutorialData } - : msg - ); - return { ...prev, messages: updatedMessages }; - }); - }, - - onFinish: (finalSession) => { - setSession(finalSession); - setSessions(prev => prev.map(s => s.sessionId === sessionId ? finalSession : s)); - }, - - onError: (error) => { - console.error('Error sending message:', error); - setSession(prev => { - if (!prev) return prev; - const updatedMessages = prev.messages.map(msg => - msg.id === assistantMessage.id - ? { ...msg, content: 'Sorry, I encountered an error. Please try again.' } - : msg - ); - return { ...prev, messages: updatedMessages }; - }); - } - } - ); - - } catch (error) { - console.error('Error sending message:', error); - setSession(prevSession => { - if (!prevSession) return prevSession; - const filteredMessages = prevSession.messages.filter(msg => - msg.id !== newMessage.id && msg.id !== assistantMessage.id - ); - return { ...prevSession, messages: filteredMessages }; - }); - } - }; - - useEffect(() => { - const foundSession = sessions.find(s => s.sessionId === sessionId); - if (foundSession) { - setSession(foundSession); - return; - } - - const storageKey = `initialMessage:${sessionId}`; - const initialMessage = sessionStorage.getItem(storageKey); - if (!initialMessage) { - return; - } - sessionStorage.removeItem(storageKey); - - const userMessage: Message = { - id: uuidv4(), - author: 'USER', - content: initialMessage, - timestamp: new Date().toISOString(), - }; - const assistantPlaceholder: Message = { - id: uuidv4(), - author: 'ASSISTANT', - content: '', - timestamp: new Date().toISOString(), - }; - const temporarySession: Session = { - sessionId: sessionId as string, - messages: [userMessage, assistantPlaceholder], - }; - - setSession(temporarySession); - - sessionService.createSession({ - sessionId: sessionId as string, - messages: [] - }) - .then(async response => { - if (response.success && response.data) { - setSession(response.data); - await handleSendMessage(initialMessage, sessionId); - } else { - setCreationError(response.error || 'Failed to create session'); - revalidateSessions?.(); - } - }) - .catch(error => { - setCreationError(error.message || 'Error creating session'); - revalidateSessions?.(); - }); - }, [sessionId]); - - - const toggleEditor = () => { setEditorOpen(false) }; - const stopServer = () => { }; - - return ( -
- {/* Non-blocking error banner */} - {creationError && ( -
-
- Error creating session: {creationError} - -
-
- )} - - - -
-
- {session ? ( - { setEditorOpen(true) }} - /> - ) : ( -
- -
- {Array.from({ length: 5 }).map((_, i) => ( -
- - -
- ))} -
-
- )} -
-
-
- {editorOpen && ( - -
- { }} - stopServer={stopServer} - editorContent={editorContent} - setEditorContent={setEditorContent} - toggleEditor={toggleEditor} - /> -
-
- )} -
-
- ); -} \ No newline at end of file diff --git a/app/chat/components/ChatInput.tsx b/app/chat/components/ChatInput.tsx deleted file mode 100644 index 0422b2ac..00000000 --- a/app/chat/components/ChatInput.tsx +++ /dev/null @@ -1,74 +0,0 @@ -'use client'; - -import { useEffect, KeyboardEvent, useState } from 'react'; -import { Send } from 'lucide-react'; -import { useAutoResize } from '../utils/useAutoResize'; - -interface ChatInputProps { - loading?: boolean; - onSendMessage: (message: string) => void; -} - -export function ChatInput({ - loading = false, - onSendMessage -}: ChatInputProps) { - const [currentInput, setCurrentInput] = useState(''); - const { textareaRef } = useAutoResize(currentInput, { maxHeight: 320 }); - - useEffect(() => { - textareaRef.current?.focus(); - }, []); - - useEffect(() => { - if (!loading) { - textareaRef.current?.focus(); - } - }, [loading]); - - const handleSend = () => { - if (currentInput.trim() && !loading) { - onSendMessage(currentInput.trim()); - setCurrentInput(''); - } - }; - - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Enter' && (!e.shiftKey || e.ctrlKey || e.metaKey)) { - e.preventDefault(); - handleSend(); - } - }; - - return ( -
- {/* Textarea Container */} -
-
-