Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/app/api/ai/pipeline/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { prisma } from "@/lib/prisma";
import { streamCompletion } from "@/server/services/ai";
import { buildPipelineSystemPrompt } from "@/lib/ai/prompts";
import { writeAuditLog } from "@/server/services/audit";
import type { AiReviewResponse } from "@/lib/ai/types";

import { Prisma } from "@/generated/prisma";

export async function POST(request: Request) {
Expand Down Expand Up @@ -184,8 +184,9 @@ export async function POST(request: Request) {
if (body.mode === "review" && conversationId) {
let parsedSuggestions = null;
try {
const parsed: AiReviewResponse = JSON.parse(fullResponse);
if (parsed.summary && Array.isArray(parsed.suggestions)) {
const { parseAiReviewResponse } = await import("@/lib/ai/suggestion-validator");
const parsed = parseAiReviewResponse(fullResponse);
if (parsed) {
parsedSuggestions = parsed.suggestions;
}
} catch {
Expand Down
7 changes: 4 additions & 3 deletions src/app/api/ai/vrl-chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { prisma } from "@/lib/prisma";
import { streamCompletion } from "@/server/services/ai";
import { buildVrlChatSystemPrompt } from "@/lib/ai/prompts";
import { writeAuditLog } from "@/server/services/audit";
import type { VrlChatResponse } from "@/lib/ai/vrl-suggestion-types";

import { Prisma } from "@/generated/prisma";

export async function POST(request: Request) {
Expand Down Expand Up @@ -174,8 +174,9 @@ export async function POST(request: Request) {
// Persist assistant response
let parsedSuggestions = null;
try {
const parsed: VrlChatResponse = JSON.parse(fullResponse);
if (parsed.summary && Array.isArray(parsed.suggestions)) {
const { parseVrlChatResponse } = await import("@/lib/ai/vrl-suggestion-types");
const parsed = parseVrlChatResponse(fullResponse);
if (parsed) {
parsedSuggestions = parsed.suggestions;
}
} catch {
Expand Down
6 changes: 3 additions & 3 deletions src/components/flow/ai-pipeline-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ScrollArea } from "@/components/ui/scroll-area";

import { useTeamStore } from "@/stores/team-store";
import { useFlowStore } from "@/stores/flow-store";
import { generateVectorYaml, importVectorConfig } from "@/lib/config-generator";
Expand Down Expand Up @@ -400,7 +400,7 @@ export function AiPipelineDialog({
) : (
<>
{/* Message thread */}
<ScrollArea className="flex-1 h-0 pr-4">
<div className="flex-1 min-h-0 overflow-y-auto pr-4">
<div className="space-y-4 pb-4">
{conversation.messages.length === 0 && !conversation.isStreaming && (
<p className="text-sm text-muted-foreground text-center py-8">
Expand Down Expand Up @@ -439,7 +439,7 @@ export function AiPipelineDialog({

<div ref={messagesEndRef} />
</div>
</ScrollArea>
</div>

{conversation.error && (
<div className="flex items-start gap-2 rounded border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive mb-3">
Expand Down
4 changes: 2 additions & 2 deletions src/components/vrl-editor/vrl-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ export function VrlEditor({ value, onChange, sourceTypes, pipelineId, componentK
<DialogContent
className="h-[85vh] flex flex-col sm:max-w-[calc(100vw-4rem)] xl:max-w-6xl"
onKeyDown={(e) => e.stopPropagation()}
onFocusOutside={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
>
<DialogHeader>
<DialogTitle>VRL Editor</DialogTitle>
Expand Down Expand Up @@ -616,7 +616,7 @@ export function VrlEditor({ value, onChange, sourceTypes, pipelineId, componentK
{pipelineId && upstreamSourceKeys && upstreamSourceKeys.length > 0 && (
<>
<Select value={String(sampleLimit)} onValueChange={(val) => setSampleLimit(Number(val))}>
<SelectTrigger className="h-8 w-[120px] text-sm">
<SelectTrigger size="sm" className="w-[120px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
Expand Down
9 changes: 8 additions & 1 deletion src/lib/ai/suggestion-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@
import type { Node } from "@xyflow/react";
import type { AiSuggestion, AiReviewResponse, SuggestionStatus } from "./types";

/** Strip markdown code fences and extract the JSON body. */
function stripCodeFences(raw: string): string {
const trimmed = raw.trim();
const match = trimmed.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?\s*```$/);
return match ? match[1].trim() : trimmed;
}

/**
* Validate a parsed AI response. Returns the response if valid, null if not.
*/
export function parseAiReviewResponse(raw: string): AiReviewResponse | null {
try {
const parsed = JSON.parse(raw);
const parsed = JSON.parse(stripCodeFences(raw));
if (
typeof parsed === "object" &&
parsed !== null &&
Expand Down
10 changes: 9 additions & 1 deletion src/lib/ai/vrl-suggestion-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,21 @@ export interface VrlChatResponse {
/** Status of a VRL suggestion in the UI */
export type VrlSuggestionStatus = "actionable" | "applied" | "outdated";

/** Strip markdown code fences and extract the JSON body. */
function stripCodeFences(raw: string): string {
const trimmed = raw.trim();
// Match ```json ... ``` or ``` ... ```
const match = trimmed.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?\s*```$/);
return match ? match[1].trim() : trimmed;
}

/**
* Parse the streamed AI response as a VrlChatResponse.
* Returns null if the response is not valid JSON or missing required fields.
*/
export function parseVrlChatResponse(raw: string): VrlChatResponse | null {
try {
const parsed = JSON.parse(raw);
const parsed = JSON.parse(stripCodeFences(raw));
if (
typeof parsed === "object" &&
parsed !== null &&
Expand Down
Loading