mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-17 18:35:19 +02:00
feat: new chat working stateless. Added citation logic.
This commit is contained in:
parent
24f438a39e
commit
947087452f
10 changed files with 441 additions and 160 deletions
|
|
@ -87,10 +87,11 @@ def create_chat_litellm_from_config(llm_config: dict) -> ChatLiteLLM | None:
|
|||
provider_prefix = provider_map.get(provider, provider.lower())
|
||||
model_string = f"{provider_prefix}/{llm_config['model_name']}"
|
||||
|
||||
# Create ChatLiteLLM instance
|
||||
# Create ChatLiteLLM instance with streaming enabled
|
||||
litellm_kwargs = {
|
||||
"model": model_string,
|
||||
"api_key": llm_config.get("api_key"),
|
||||
"streaming": True, # Enable streaming for real-time token streaming
|
||||
}
|
||||
|
||||
# Add optional parameters
|
||||
|
|
|
|||
|
|
@ -12,10 +12,9 @@ from langchain_core.messages import HumanMessage
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.new_chat.chat_deepagent import (
|
||||
create_chat_litellm_from_config,
|
||||
create_surfsense_deep_agent,
|
||||
load_llm_config_from_yaml,
|
||||
)
|
||||
from app.agents.new_chat.llm_config import create_chat_litellm_from_config, load_llm_config_from_yaml
|
||||
from app.services.connector_service import ConnectorService
|
||||
from app.services.new_streaming_service import VercelStreamingService
|
||||
|
||||
|
|
|
|||
|
|
@ -1,74 +0,0 @@
|
|||
import type { UIMessage } from "ai";
|
||||
|
||||
export const maxDuration = 30;
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const {
|
||||
messages,
|
||||
chat_id,
|
||||
search_space_id,
|
||||
}: {
|
||||
messages: UIMessage[];
|
||||
chat_id?: number;
|
||||
search_space_id?: number;
|
||||
} = body;
|
||||
|
||||
// Get auth token from headers
|
||||
const authHeader = req.headers.get("authorization");
|
||||
if (!authHeader) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
// Get the last user message
|
||||
const lastUserMessage = messages.filter((m) => m.role === "user").pop();
|
||||
|
||||
if (!lastUserMessage) {
|
||||
return new Response("No user message found", { status: 400 });
|
||||
}
|
||||
|
||||
// Extract text content from the message
|
||||
const userQuery =
|
||||
typeof lastUserMessage.content === "string"
|
||||
? lastUserMessage.content
|
||||
: lastUserMessage.content
|
||||
.filter((c: any) => c.type === "text")
|
||||
.map((c: any) => c.text)
|
||||
.join(" ");
|
||||
|
||||
// Call the DeepAgent backend
|
||||
const backendUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
|
||||
const response = await fetch(`${backendUrl}/api/v1/new_chat`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: authHeader,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chat_id: chat_id || 0,
|
||||
user_query: userQuery,
|
||||
search_space_id: search_space_id || 0,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return new Response(`Backend error: ${response.statusText}`, {
|
||||
status: response.status,
|
||||
});
|
||||
}
|
||||
|
||||
// The backend returns SSE stream with Vercel AI SDK Data Stream Protocol
|
||||
// We need to forward this stream to the client
|
||||
return new Response(response.body, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error in deep-agent-chat route:", error);
|
||||
return new Response("Internal Server Error", { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,44 @@
|
|||
"use client";
|
||||
|
||||
import { AssistantRuntimeProvider } from "@assistant-ui/react";
|
||||
import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
|
||||
import { AssistantRuntimeProvider, useLocalRuntime } from "@assistant-ui/react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useMemo } from "react";
|
||||
import { Thread } from "@/components/assistant-ui/thread";
|
||||
import { createNewChatAdapter } from "@/lib/chat/new-chat-transport";
|
||||
|
||||
export default function NewChatPage() {
|
||||
// Using the official assistant-ui pattern - useChatRuntime with NO parameters
|
||||
// It defaults to /api/chat endpoint
|
||||
const runtime = useChatRuntime();
|
||||
const params = useParams();
|
||||
|
||||
// Extract search_space_id and chat_id from URL params
|
||||
const searchSpaceId = useMemo(() => {
|
||||
const id = params.search_space_id;
|
||||
const parsed = typeof id === "string" ? Number.parseInt(id, 10) : 0;
|
||||
return Number.isNaN(parsed) ? 0 : parsed;
|
||||
}, [params.search_space_id]);
|
||||
|
||||
const chatId = useMemo(() => {
|
||||
const id = params.chat_id;
|
||||
let parsed = 0;
|
||||
if (Array.isArray(id) && id.length > 0) {
|
||||
parsed = Number.parseInt(id[0], 10);
|
||||
} else if (typeof id === "string") {
|
||||
parsed = Number.parseInt(id, 10);
|
||||
}
|
||||
return Number.isNaN(parsed) ? 0 : parsed;
|
||||
}, [params.chat_id]);
|
||||
|
||||
// Create the adapter with the extracted params
|
||||
const adapter = useMemo(
|
||||
() => createNewChatAdapter({ searchSpaceId, chatId }),
|
||||
[searchSpaceId, chatId]
|
||||
);
|
||||
|
||||
// Use LocalRuntime with our custom adapter
|
||||
const runtime = useLocalRuntime(adapter);
|
||||
|
||||
return (
|
||||
<AssistantRuntimeProvider runtime={runtime}>
|
||||
<div className="h-full">
|
||||
<div className="h-[calc(100vh-64px)] max-h-[calc(100vh-64px)] overflow-hidden">
|
||||
<Thread />
|
||||
</div>
|
||||
</AssistantRuntimeProvider>
|
||||
|
|
|
|||
47
surfsense_web/components/assistant-ui/inline-citation.tsx
Normal file
47
surfsense_web/components/assistant-ui/inline-citation.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { useState } from "react";
|
||||
import { SheetTrigger } from "@/components/ui/sheet";
|
||||
import { SourceDetailSheet } from "@/components/chat/SourceDetailSheet";
|
||||
|
||||
interface InlineCitationProps {
|
||||
chunkId: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline citation component for the new chat.
|
||||
* Renders a clickable badge that opens the SourceDetailSheet with document chunk details.
|
||||
*/
|
||||
export const InlineCitation: FC<InlineCitationProps> = ({ chunkId }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<SourceDetailSheet
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
chunkId={chunkId}
|
||||
sourceType=""
|
||||
title="Source"
|
||||
description=""
|
||||
url=""
|
||||
>
|
||||
<SheetTrigger asChild>
|
||||
<span
|
||||
className="text-[10px] font-bold bg-primary/80 hover:bg-primary text-primary-foreground rounded-full w-4 h-4 inline-flex items-center justify-center align-super cursor-pointer transition-colors ml-0.5"
|
||||
title={`View source (chunk ${chunkId})`}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
className="w-2.5 h-2.5"
|
||||
>
|
||||
<path d="M6.22 8.72a.75.75 0 0 0 1.06 1.06l5.22-5.22v1.69a.75.75 0 0 0 1.5 0v-3.5a.75.75 0 0 0-.75-.75h-3.5a.75.75 0 0 0 0 1.5h1.69L6.22 8.72Z" />
|
||||
<path d="M3.5 6.75c0-.69.56-1.25 1.25-1.25H7A.75.75 0 0 0 7 4H4.75A2.75 2.75 0 0 0 2 6.75v4.5A2.75 2.75 0 0 0 4.75 14h4.5A2.75 2.75 0 0 0 12 11.25V9a.75.75 0 0 0-1.5 0v2.25c0 .69-.56 1.25-1.25 1.25h-4.5c-.69 0-1.25-.56-1.25-1.25v-4.5Z" />
|
||||
</svg>
|
||||
</span>
|
||||
</SheetTrigger>
|
||||
</SourceDetailSheet>
|
||||
);
|
||||
};
|
||||
|
|
@ -9,12 +9,52 @@ import {
|
|||
useIsMarkdownCodeBlock,
|
||||
} from "@assistant-ui/react-markdown";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { type FC, memo, useState } from "react";
|
||||
import { type FC, type ReactNode, memo, useState } from "react";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||
import { InlineCitation } from "@/components/assistant-ui/inline-citation";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// Citation pattern: [citation:CHUNK_ID]
|
||||
const CITATION_REGEX = /\[citation:(\d+)\]/g;
|
||||
|
||||
/**
|
||||
* Parses text and replaces [citation:XXX] patterns with InlineCitation components
|
||||
*/
|
||||
function parseTextWithCitations(text: string): ReactNode[] {
|
||||
const parts: ReactNode[] = [];
|
||||
let lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
let citationIndex = 0;
|
||||
|
||||
// Reset regex state
|
||||
CITATION_REGEX.lastIndex = 0;
|
||||
|
||||
while ((match = CITATION_REGEX.exec(text)) !== null) {
|
||||
// Add text before the citation
|
||||
if (match.index > lastIndex) {
|
||||
parts.push(text.substring(lastIndex, match.index));
|
||||
}
|
||||
|
||||
// Add the citation component
|
||||
const chunkId = Number.parseInt(match[1], 10);
|
||||
parts.push(
|
||||
<InlineCitation key={`citation-${chunkId}-${citationIndex}`} chunkId={chunkId} />
|
||||
);
|
||||
|
||||
lastIndex = match.index + match[0].length;
|
||||
citationIndex++;
|
||||
}
|
||||
|
||||
// Add any remaining text after the last citation
|
||||
if (lastIndex < text.length) {
|
||||
parts.push(text.substring(lastIndex));
|
||||
}
|
||||
|
||||
return parts.length > 0 ? parts : [text];
|
||||
}
|
||||
|
||||
const MarkdownTextImpl = () => {
|
||||
return (
|
||||
<MarkdownTextPrimitive
|
||||
|
|
@ -60,63 +100,116 @@ const useCopyToClipboard = ({ copiedDuration = 3000 }: { copiedDuration?: number
|
|||
return { isCopied, copyToClipboard };
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to process children and replace citation patterns with components
|
||||
*/
|
||||
function processChildrenWithCitations(children: ReactNode): ReactNode {
|
||||
if (typeof children === "string") {
|
||||
const parsed = parseTextWithCitations(children);
|
||||
return parsed.length === 1 && typeof parsed[0] === "string" ? children : <>{parsed}</>;
|
||||
}
|
||||
|
||||
if (Array.isArray(children)) {
|
||||
return children.map((child, index) => {
|
||||
if (typeof child === "string") {
|
||||
const parsed = parseTextWithCitations(child);
|
||||
return parsed.length === 1 && typeof parsed[0] === "string" ? (
|
||||
child
|
||||
) : (
|
||||
<span key={index}>{parsed}</span>
|
||||
);
|
||||
}
|
||||
return child;
|
||||
});
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
const defaultComponents = memoizeMarkdownComponents({
|
||||
h1: ({ className, ...props }) => (
|
||||
h1: ({ className, children, ...props }) => (
|
||||
<h1
|
||||
className={cn(
|
||||
"aui-md-h1 mb-8 scroll-m-20 font-extrabold text-4xl tracking-tight last:mb-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{processChildrenWithCitations(children)}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ className, ...props }) => (
|
||||
h2: ({ className, children, ...props }) => (
|
||||
<h2
|
||||
className={cn(
|
||||
"aui-md-h2 mt-8 mb-4 scroll-m-20 font-semibold text-3xl tracking-tight first:mt-0 last:mb-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{processChildrenWithCitations(children)}
|
||||
</h2>
|
||||
),
|
||||
h3: ({ className, ...props }) => (
|
||||
h3: ({ className, children, ...props }) => (
|
||||
<h3
|
||||
className={cn(
|
||||
"aui-md-h3 mt-6 mb-4 scroll-m-20 font-semibold text-2xl tracking-tight first:mt-0 last:mb-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{processChildrenWithCitations(children)}
|
||||
</h3>
|
||||
),
|
||||
h4: ({ className, ...props }) => (
|
||||
h4: ({ className, children, ...props }) => (
|
||||
<h4
|
||||
className={cn(
|
||||
"aui-md-h4 mt-6 mb-4 scroll-m-20 font-semibold text-xl tracking-tight first:mt-0 last:mb-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{processChildrenWithCitations(children)}
|
||||
</h4>
|
||||
),
|
||||
h5: ({ className, ...props }) => (
|
||||
h5: ({ className, children, ...props }) => (
|
||||
<h5
|
||||
className={cn("aui-md-h5 my-4 font-semibold text-lg first:mt-0 last:mb-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{processChildrenWithCitations(children)}
|
||||
</h5>
|
||||
),
|
||||
h6: ({ className, ...props }) => (
|
||||
<h6 className={cn("aui-md-h6 my-4 font-semibold first:mt-0 last:mb-0", className)} {...props} />
|
||||
h6: ({ className, children, ...props }) => (
|
||||
<h6
|
||||
className={cn("aui-md-h6 my-4 font-semibold first:mt-0 last:mb-0", className)}
|
||||
{...props}
|
||||
>
|
||||
{processChildrenWithCitations(children)}
|
||||
</h6>
|
||||
),
|
||||
p: ({ className, ...props }) => (
|
||||
<p className={cn("aui-md-p mt-5 mb-5 leading-7 first:mt-0 last:mb-0", className)} {...props} />
|
||||
p: ({ className, children, ...props }) => (
|
||||
<p
|
||||
className={cn("aui-md-p mt-5 mb-5 leading-7 first:mt-0 last:mb-0", className)}
|
||||
{...props}
|
||||
>
|
||||
{processChildrenWithCitations(children)}
|
||||
</p>
|
||||
),
|
||||
a: ({ className, ...props }) => (
|
||||
a: ({ className, children, ...props }) => (
|
||||
<a
|
||||
className={cn("aui-md-a font-medium text-primary underline underline-offset-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{processChildrenWithCitations(children)}
|
||||
</a>
|
||||
),
|
||||
blockquote: ({ className, ...props }) => (
|
||||
<blockquote className={cn("aui-md-blockquote border-l-2 pl-6 italic", className)} {...props} />
|
||||
blockquote: ({ className, children, ...props }) => (
|
||||
<blockquote
|
||||
className={cn("aui-md-blockquote border-l-2 pl-6 italic", className)}
|
||||
{...props}
|
||||
>
|
||||
{processChildrenWithCitations(children)}
|
||||
</blockquote>
|
||||
),
|
||||
ul: ({ className, ...props }) => (
|
||||
<ul className={cn("aui-md-ul my-5 ml-6 list-disc [&>li]:mt-2", className)} {...props} />
|
||||
|
|
@ -124,6 +217,11 @@ const defaultComponents = memoizeMarkdownComponents({
|
|||
ol: ({ className, ...props }) => (
|
||||
<ol className={cn("aui-md-ol my-5 ml-6 list-decimal [&>li]:mt-2", className)} {...props} />
|
||||
),
|
||||
li: ({ className, children, ...props }) => (
|
||||
<li className={cn("aui-md-li", className)} {...props}>
|
||||
{processChildrenWithCitations(children)}
|
||||
</li>
|
||||
),
|
||||
hr: ({ className, ...props }) => (
|
||||
<hr className={cn("aui-md-hr my-5 border-b", className)} {...props} />
|
||||
),
|
||||
|
|
@ -136,23 +234,27 @@ const defaultComponents = memoizeMarkdownComponents({
|
|||
{...props}
|
||||
/>
|
||||
),
|
||||
th: ({ className, ...props }) => (
|
||||
th: ({ className, children, ...props }) => (
|
||||
<th
|
||||
className={cn(
|
||||
"aui-md-th bg-muted px-4 py-2 text-left font-bold first:rounded-tl-lg last:rounded-tr-lg [[align=center]]:text-center [[align=right]]:text-right",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{processChildrenWithCitations(children)}
|
||||
</th>
|
||||
),
|
||||
td: ({ className, ...props }) => (
|
||||
td: ({ className, children, ...props }) => (
|
||||
<td
|
||||
className={cn(
|
||||
"aui-md-td border-b border-l px-4 py-2 text-left last:border-r [[align=center]]:text-center [[align=right]]:text-right",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{processChildrenWithCitations(children)}
|
||||
</td>
|
||||
),
|
||||
tr: ({ className, ...props }) => (
|
||||
<tr
|
||||
|
|
@ -187,5 +289,15 @@ const defaultComponents = memoizeMarkdownComponents({
|
|||
/>
|
||||
);
|
||||
},
|
||||
strong: ({ className, children, ...props }) => (
|
||||
<strong className={cn("aui-md-strong font-semibold", className)} {...props}>
|
||||
{processChildrenWithCitations(children)}
|
||||
</strong>
|
||||
),
|
||||
em: ({ className, children, ...props }) => (
|
||||
<em className={cn("aui-md-em", className)} {...props}>
|
||||
{processChildrenWithCitations(children)}
|
||||
</em>
|
||||
),
|
||||
CodeHeader,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ const ThreadSuggestions: FC = () => {
|
|||
className="aui-thread-welcome-suggestion-display fade-in slide-in-from-bottom-2 @md:nth-[n+3]:block nth-[n+3]:hidden animate-in fill-mode-both duration-200"
|
||||
style={{ animationDelay: `${100 + index * 50}ms` }}
|
||||
>
|
||||
<ThreadPrimitive.Suggestion prompt={suggestion.prompt} send asChild>
|
||||
<ThreadPrimitive.Suggestion prompt={suggestion.prompt} autoSend asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="aui-thread-welcome-suggestion h-auto w-full @md:flex-col flex-wrap items-start justify-start gap-1 rounded-2xl border px-4 py-3 text-left text-sm transition-colors hover:bg-muted"
|
||||
|
|
|
|||
167
surfsense_web/lib/chat/new-chat-transport.ts
Normal file
167
surfsense_web/lib/chat/new-chat-transport.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
/**
|
||||
* Custom ChatModelAdapter for the new-chat feature using LocalRuntime.
|
||||
* Connects directly to the FastAPI backend using the Vercel AI SDK Data Stream Protocol.
|
||||
*/
|
||||
|
||||
import type { ChatModelAdapter, ChatModelRunOptions } from "@assistant-ui/react";
|
||||
import { getBearerToken } from "@/lib/auth-utils";
|
||||
|
||||
interface NewChatAdapterConfig {
|
||||
searchSpaceId: number;
|
||||
chatId: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ChatModelAdapter that connects to the FastAPI new_chat endpoint.
|
||||
*
|
||||
* The backend expects:
|
||||
* - POST /api/v1/new_chat
|
||||
* - Body: { chat_id: number, user_query: string, search_space_id: number }
|
||||
* - Returns: SSE stream with Vercel AI SDK Data Stream Protocol
|
||||
*/
|
||||
export function createNewChatAdapter(config: NewChatAdapterConfig): ChatModelAdapter {
|
||||
const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
|
||||
|
||||
return {
|
||||
async *run({ messages, abortSignal }: ChatModelRunOptions) {
|
||||
// Get the last user message
|
||||
const lastUserMessage = messages.filter((m) => m.role === "user").pop();
|
||||
|
||||
if (!lastUserMessage) {
|
||||
throw new Error("No user message found");
|
||||
}
|
||||
|
||||
// Extract text content from the message
|
||||
let userQuery = "";
|
||||
for (const part of lastUserMessage.content) {
|
||||
if (part.type === "text") {
|
||||
userQuery += part.text;
|
||||
}
|
||||
}
|
||||
|
||||
if (!userQuery.trim()) {
|
||||
throw new Error("User query cannot be empty");
|
||||
}
|
||||
|
||||
const token = getBearerToken();
|
||||
if (!token) {
|
||||
throw new Error("Not authenticated. Please log in again.");
|
||||
}
|
||||
|
||||
const response = await fetch(`${backendUrl}/api/v1/new_chat`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chat_id: config.chatId,
|
||||
user_query: userQuery.trim(),
|
||||
search_space_id: config.searchSpaceId,
|
||||
}),
|
||||
signal: abortSignal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => "Unknown error");
|
||||
throw new Error(`Backend error (${response.status}): ${errorText}`);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error("No response body");
|
||||
}
|
||||
|
||||
// Parse the SSE stream (Vercel AI SDK Data Stream Protocol)
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
let accumulatedText = "";
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
buffer += chunk;
|
||||
|
||||
// Split on double newlines (handle both \n\n and \r\n\r\n)
|
||||
const events = buffer.split(/\r?\n\r?\n/);
|
||||
buffer = events.pop() || "";
|
||||
|
||||
for (const event of events) {
|
||||
// Each event can have multiple lines, find the data line
|
||||
const lines = event.split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith("data: ")) continue;
|
||||
|
||||
const data = line.slice(6).trim(); // Remove "data: " prefix
|
||||
|
||||
// Handle [DONE] marker
|
||||
if (data === "[DONE]") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!data) continue;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
// Handle different message types from the Data Stream Protocol
|
||||
switch (parsed.type) {
|
||||
case "text-delta":
|
||||
accumulatedText += parsed.delta;
|
||||
yield {
|
||||
content: [{ type: "text" as const, text: accumulatedText }],
|
||||
};
|
||||
break;
|
||||
|
||||
case "error":
|
||||
throw new Error(parsed.errorText || "Unknown error from server");
|
||||
|
||||
// Other types like text-start, text-end, tool-*, etc.
|
||||
// are handled implicitly - we just accumulate text deltas
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip non-JSON lines
|
||||
if (e instanceof SyntaxError) {
|
||||
continue;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle any remaining buffer
|
||||
if (buffer.trim()) {
|
||||
const lines = buffer.split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("data: ")) {
|
||||
const data = line.slice(6).trim();
|
||||
if (data && data !== "[DONE]") {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
if (parsed.type === "text-delta") {
|
||||
accumulatedText += parsed.delta;
|
||||
yield {
|
||||
content: [{ type: "text" as const, text: accumulatedText }],
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -22,9 +22,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/react": "^1.2.12",
|
||||
"@assistant-ui/react": "^0.11.52",
|
||||
"@assistant-ui/react-ai-sdk": "^1.1.19",
|
||||
"@assistant-ui/react-markdown": "^0.11.8",
|
||||
"@assistant-ui/react": "^0.11.53",
|
||||
"@assistant-ui/react-ai-sdk": "^1.1.20",
|
||||
"@assistant-ui/react-markdown": "^0.11.9",
|
||||
"@blocknote/core": "^0.45.0",
|
||||
"@blocknote/mantine": "^0.45.0",
|
||||
"@blocknote/react": "^0.45.0",
|
||||
|
|
@ -100,7 +100,9 @@
|
|||
"sonner": "^2.0.6",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^4.2.1"
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"zod": "^4.2.1",
|
||||
"zustand": "^5.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.1.2",
|
||||
|
|
|
|||
98
surfsense_web/pnpm-lock.yaml
generated
98
surfsense_web/pnpm-lock.yaml
generated
|
|
@ -12,14 +12,14 @@ importers:
|
|||
specifier: ^1.2.12
|
||||
version: 1.2.12(react@19.2.3)(zod@4.2.1)
|
||||
'@assistant-ui/react':
|
||||
specifier: ^0.11.52
|
||||
version: 0.11.52(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3))
|
||||
specifier: ^0.11.53
|
||||
version: 0.11.53(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3))
|
||||
'@assistant-ui/react-ai-sdk':
|
||||
specifier: ^1.1.19
|
||||
version: 1.1.19(@assistant-ui/react@0.11.52(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)))(@types/react@19.2.7)(assistant-cloud@0.1.11)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3))
|
||||
specifier: ^1.1.20
|
||||
version: 1.1.20(@assistant-ui/react@0.11.53(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)))(@types/react@19.2.7)(assistant-cloud@0.1.12)(react@19.2.3)
|
||||
'@assistant-ui/react-markdown':
|
||||
specifier: ^0.11.8
|
||||
version: 0.11.8(@assistant-ui/react@0.11.52(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)))(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
specifier: ^0.11.9
|
||||
version: 0.11.9(@assistant-ui/react@0.11.53(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)))(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@blocknote/core':
|
||||
specifier: ^0.45.0
|
||||
version: 0.45.0(@tiptap/extensions@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0))(@types/hast@3.0.4)(highlight.js@11.11.1)
|
||||
|
|
@ -245,9 +245,15 @@ importers:
|
|||
tailwindcss-animate:
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7(tailwindcss@4.1.18)
|
||||
unist-util-visit:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
zod:
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1
|
||||
zustand:
|
||||
specifier: ^5.0.9
|
||||
version: 5.0.9(@types/react@19.2.7)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3))
|
||||
devDependencies:
|
||||
'@biomejs/biome':
|
||||
specifier: 2.1.2
|
||||
|
|
@ -359,47 +365,50 @@ packages:
|
|||
'@asamuzakjp/css-color@3.2.0':
|
||||
resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
|
||||
|
||||
'@assistant-ui/react-ai-sdk@1.1.19':
|
||||
resolution: {integrity: sha512-Xlwsnm9Uaq/SlBKtQcGJgNALMr0uHvqt9r54QdrvM+/PfZV7NQkDZubHzx4WwS0uNBh3Gh62zvPchlfZnBdiaA==}
|
||||
'@assistant-ui/react-ai-sdk@1.1.20':
|
||||
resolution: {integrity: sha512-1t+TBUIeNwq7ukb3rLMeSnPeQHrCj5LdwOuvqkYvx5d7dspNMUd2Zh954Gxdie0/iLHGn3ltpjscZeJWSrjSxg==}
|
||||
peerDependencies:
|
||||
'@assistant-ui/react': ^0.11.50
|
||||
'@assistant-ui/react': ^0.11.53
|
||||
'@types/react': '*'
|
||||
assistant-cloud: '*'
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
react: ^18 || ^19
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
assistant-cloud:
|
||||
optional: true
|
||||
|
||||
'@assistant-ui/react-markdown@0.11.8':
|
||||
resolution: {integrity: sha512-Us7yD9xUGozmmmDuWn+lCp4bylA2sINWyGE6RXr0onmAlwXcOUaw1ZgofKscJXkw6DmC0jrLj7Bdsw8O4IUJhw==}
|
||||
'@assistant-ui/react-markdown@0.11.9':
|
||||
resolution: {integrity: sha512-zR0Ty4ID5htJgm4g1TVAbTsyfJZ8XHccDQ0sMODsq/PWAM75l7EmAbxdSKPbvCqny1A/FxvAB4dz1LA17ZgoWg==}
|
||||
peerDependencies:
|
||||
'@assistant-ui/react': ^0.11.50
|
||||
'@assistant-ui/react': ^0.11.53
|
||||
'@types/react': '*'
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
react: ^18 || ^19
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@assistant-ui/react@0.11.52':
|
||||
resolution: {integrity: sha512-7lM6IfU9o82wqpOj1wZYI71NQ3jt5OkCh893pvc5utpeJUCkxGKvdx7bRGdMN1XsC14//y7Z4T9bQ16sHtoTjw==}
|
||||
'@assistant-ui/react@0.11.53':
|
||||
resolution: {integrity: sha512-G5VB752Somw2Xv4JkGqnloZTxXRu2laHufOROs2H9yOE9Pu+o9aCjW/rn9p8FIev4gWh/ltDouX8T+z9Fh8dJw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^18 || ^19 || ^19.0.0-rc
|
||||
react: ^18 || ^19
|
||||
react-dom: ^18 || ^19
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@assistant-ui/tap@0.3.4':
|
||||
resolution: {integrity: sha512-62Fz1y4EJA7Hk+aStjNS9VTciZ/TUf9SKdv5RfGF65A1Y/REOBzxBDL4ZCeGHCX2dxmwESg5/Kehky+57og3EA==}
|
||||
'@assistant-ui/tap@0.3.5':
|
||||
resolution: {integrity: sha512-aI7lOKglkVYy17GrS9EdjSrOmEBmofWPBZ4F5wb96yqEynXflXY3qUAFCgmUwaP/TVkog72+o1ePyvsGphSmJQ==}
|
||||
peerDependencies:
|
||||
react: '*'
|
||||
'@types/react': '*'
|
||||
react: ^18 || ^19
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
|
||||
|
|
@ -3344,11 +3353,11 @@ packages:
|
|||
asap@2.0.6:
|
||||
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
|
||||
|
||||
assistant-cloud@0.1.11:
|
||||
resolution: {integrity: sha512-1ZS7ccwiuy1NxNzIbHKubdCEcqe914YXVYxAR9N3M4csiLdrtUC1/U7KgLXy5Sig2Jp951L/wnN2fLzl302+sA==}
|
||||
assistant-cloud@0.1.12:
|
||||
resolution: {integrity: sha512-A2tY6QIdP9+RkE8Mmpm4kAoO0NyKsKpJKYebbYFZ3bAnQKyB15Bw/PS9AovpdeziGU9At97TyiMrT36pDjCD7A==}
|
||||
|
||||
assistant-stream@0.2.45:
|
||||
resolution: {integrity: sha512-gfCwkPcGpfCpwWDzKrgzR5TvKP/ppc+MqotFHPCufbYK8nMft1+GyWGhQp6XXzQR2dC7T/IrqmtEGlGSs8uobA==}
|
||||
assistant-stream@0.2.46:
|
||||
resolution: {integrity: sha512-smcC4sqOcTrUO01YpiHPgdG3Wc57kmQlCIEdMXSNuWMgcDvo60hnRY3rPDhZQBJHZOXQ9Q1wLR8ugKDjxi72GQ==}
|
||||
|
||||
ast-types-flow@0.0.8:
|
||||
resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
|
||||
|
|
@ -6790,31 +6799,22 @@ snapshots:
|
|||
'@csstools/css-tokenizer': 3.0.4
|
||||
lru-cache: 10.4.3
|
||||
|
||||
'@assistant-ui/react-ai-sdk@1.1.19(@assistant-ui/react@0.11.52(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)))(@types/react@19.2.7)(assistant-cloud@0.1.11)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3))':
|
||||
'@assistant-ui/react-ai-sdk@1.1.20(@assistant-ui/react@0.11.53(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)))(@types/react@19.2.7)(assistant-cloud@0.1.12)(react@19.2.3)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@ai-sdk/react': 2.0.118(react@19.2.3)(zod@4.2.1)
|
||||
'@assistant-ui/react': 0.11.52(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3))
|
||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.3)
|
||||
'@types/json-schema': 7.0.15
|
||||
'@assistant-ui/react': 0.11.53(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3))
|
||||
ai: 5.0.116(zod@4.2.1)
|
||||
assistant-stream: 0.2.45
|
||||
react: 19.2.3
|
||||
zod: 4.2.1
|
||||
zustand: 5.0.9(@types/react@19.2.7)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3))
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.7
|
||||
assistant-cloud: 0.1.11
|
||||
transitivePeerDependencies:
|
||||
- immer
|
||||
- use-sync-external-store
|
||||
assistant-cloud: 0.1.12
|
||||
|
||||
'@assistant-ui/react-markdown@0.11.8(@assistant-ui/react@0.11.52(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)))(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
'@assistant-ui/react-markdown@0.11.9(@assistant-ui/react@0.11.53(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)))(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
'@assistant-ui/react': 0.11.52(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3))
|
||||
'@assistant-ui/react': 0.11.53(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3))
|
||||
'@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.3)
|
||||
'@types/hast': 3.0.4
|
||||
classnames: 2.5.1
|
||||
react: 19.2.3
|
||||
react-markdown: 10.1.0(@types/react@19.2.7)(react@19.2.3)
|
||||
|
|
@ -6825,9 +6825,9 @@ snapshots:
|
|||
- react-dom
|
||||
- supports-color
|
||||
|
||||
'@assistant-ui/react@0.11.52(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3))':
|
||||
'@assistant-ui/react@0.11.53(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3))':
|
||||
dependencies:
|
||||
'@assistant-ui/tap': 0.3.4(react@19.2.3)
|
||||
'@assistant-ui/tap': 0.3.5(@types/react@19.2.7)(react@19.2.3)
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3)
|
||||
'@radix-ui/react-context': 1.1.3(@types/react@19.2.7)(react@19.2.3)
|
||||
|
|
@ -6836,9 +6836,8 @@ snapshots:
|
|||
'@radix-ui/react-slot': 1.2.4(@types/react@19.2.7)(react@19.2.3)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.3)
|
||||
'@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.7)(react@19.2.3)
|
||||
'@standard-schema/spec': 1.1.0
|
||||
assistant-cloud: 0.1.11
|
||||
assistant-stream: 0.2.45
|
||||
assistant-cloud: 0.1.12
|
||||
assistant-stream: 0.2.46
|
||||
nanoid: 5.1.6
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
|
|
@ -6852,8 +6851,9 @@ snapshots:
|
|||
- immer
|
||||
- use-sync-external-store
|
||||
|
||||
'@assistant-ui/tap@0.3.4(react@19.2.3)':
|
||||
'@assistant-ui/tap@0.3.5(@types/react@19.2.7)(react@19.2.3)':
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.7
|
||||
react: 19.2.3
|
||||
|
||||
'@babel/runtime@7.28.4': {}
|
||||
|
|
@ -10119,13 +10119,13 @@ snapshots:
|
|||
|
||||
asap@2.0.6: {}
|
||||
|
||||
assistant-cloud@0.1.11:
|
||||
assistant-cloud@0.1.12:
|
||||
dependencies:
|
||||
assistant-stream: 0.2.45
|
||||
assistant-stream: 0.2.46
|
||||
|
||||
assistant-stream@0.2.45:
|
||||
assistant-stream@0.2.46:
|
||||
dependencies:
|
||||
'@types/json-schema': 7.0.15
|
||||
'@standard-schema/spec': 1.1.0
|
||||
nanoid: 5.1.6
|
||||
secure-json-parse: 4.1.0
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue