feat: adds what is openrag prompt, refactors chat design, adds scroll to bottom on chat, adds streaming support (#283)
* Changed prompts to include info about OpenRAG, change status of As Dataframe and As Vector Store to false on OpenSearch component * added markdown to onboarding step * added className to markdown renderer * changed onboarding step to not render span * Added nudges to onboarding content * Added onboarding style for nudges * updated user message and assistant message designs * updated route.ts to handle streaming messages * created new useChatStreaming to handle streaming * changed useChatStreaming to work with the chat page * changed onboarding content to use default messages instead of onboarding steps, and to use the new hook to send messages * added span to the markdown renderer on stream * updated page to use new chat streaming hook * disable animation on completed steps * changed markdown renderer margins * changed css to not display markdown links and texts on white always * added isCompleted to assistant and user messages * removed space between elements on onboarding step to ensure smoother animation * removed opacity 50 on onboarding messages * changed default api to be langflow on chat streaming * added fade in and color transition * added color transition * Rendered onboarding with use-stick-to-bottom * Added use stick to bottom on page * fixed nudges design * changed chat input design * fixed nudges design * made overflow be hidden on main * Added overflow y auto on other pages * Put animate on messages * Add source to types * Adds animate and delay props to messages
This commit is contained in:
parent
c5447f6c5d
commit
fcf7a302d0
18 changed files with 2660 additions and 2601 deletions
|
|
@ -1261,7 +1261,7 @@
|
||||||
"display_name": "as_dataframe",
|
"display_name": "as_dataframe",
|
||||||
"name": "as_dataframe",
|
"name": "as_dataframe",
|
||||||
"readonly": false,
|
"readonly": false,
|
||||||
"status": true,
|
"status": false,
|
||||||
"tags": [
|
"tags": [
|
||||||
"as_dataframe"
|
"as_dataframe"
|
||||||
]
|
]
|
||||||
|
|
@ -1280,7 +1280,7 @@
|
||||||
"display_name": "as_vector_store",
|
"display_name": "as_vector_store",
|
||||||
"name": "as_vector_store",
|
"name": "as_vector_store",
|
||||||
"readonly": false,
|
"readonly": false,
|
||||||
"status": true,
|
"status": false,
|
||||||
"tags": [
|
"tags": [
|
||||||
"as_vector_store"
|
"as_vector_store"
|
||||||
]
|
]
|
||||||
|
|
@ -2086,7 +2086,7 @@
|
||||||
"trace_as_input": true,
|
"trace_as_input": true,
|
||||||
"trace_as_metadata": true,
|
"trace_as_metadata": true,
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"value": "You are a helpful assistant that can use tools to answer questions and perform tasks."
|
"value": "You are a helpful assistant that can use tools to answer questions and perform tasks. You are part of OpenRAG, an assistant that analyzes documents and provides informations about them. When asked about what is OpenRAG, answer the following:\n\n\"OpenRAG is an open-source package for building agentic RAG systems. It supports integration with a wide range of orchestration tools, vector databases, and LLM providers. OpenRAG connects and amplifies three popular, proven open-source projects into one powerful platform:\n\n**Langflow** – Langflow is a powerful tool to build and deploy AI agents and MCP servers [Read more](https://www.langflow.org/)\n\n**OpenSearch** – Langflow is a powerful tool to build and deploy AI agents and MCP servers [Read more](https://opensearch.org/)\n\n**Docling** – Langflow is a powerful tool to build and deploy AI agents and MCP servers [Read more](https://www.docling.ai/)\""
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"_input_type": "HandleInput",
|
"_input_type": "HandleInput",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import CodeComponent from "./code-component";
|
||||||
|
|
||||||
type MarkdownRendererProps = {
|
type MarkdownRendererProps = {
|
||||||
chatMessage: string;
|
chatMessage: string;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const preprocessChatMessage = (text: string): string => {
|
const preprocessChatMessage = (text: string): string => {
|
||||||
|
|
@ -48,7 +49,7 @@ export const cleanupTableEmptyCells = (text: string): string => {
|
||||||
})
|
})
|
||||||
.join("\n");
|
.join("\n");
|
||||||
};
|
};
|
||||||
export const MarkdownRenderer = ({ chatMessage }: MarkdownRendererProps) => {
|
export const MarkdownRenderer = ({ chatMessage, className }: MarkdownRendererProps) => {
|
||||||
// Process the chat message to handle <think> tags and clean up tables
|
// Process the chat message to handle <think> tags and clean up tables
|
||||||
const processedChatMessage = preprocessChatMessage(chatMessage);
|
const processedChatMessage = preprocessChatMessage(chatMessage);
|
||||||
|
|
||||||
|
|
@ -57,6 +58,7 @@ export const MarkdownRenderer = ({ chatMessage }: MarkdownRendererProps) => {
|
||||||
className={cn(
|
className={cn(
|
||||||
"markdown prose flex w-full max-w-full flex-col items-baseline text-base font-normal word-break-break-word dark:prose-invert",
|
"markdown prose flex w-full max-w-full flex-col items-baseline text-base font-normal word-break-break-word dark:prose-invert",
|
||||||
!chatMessage ? "text-muted-foreground" : "text-primary",
|
!chatMessage ? "text-muted-foreground" : "text-primary",
|
||||||
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Markdown
|
<Markdown
|
||||||
|
|
@ -65,11 +67,14 @@ export const MarkdownRenderer = ({ chatMessage }: MarkdownRendererProps) => {
|
||||||
urlTransform={(url) => url}
|
urlTransform={(url) => url}
|
||||||
components={{
|
components={{
|
||||||
p({ node, ...props }) {
|
p({ node, ...props }) {
|
||||||
return <p className="w-fit max-w-full">{props.children}</p>;
|
return <p className="w-fit max-w-full first:mt-0 last:mb-0 my-2">{props.children}</p>;
|
||||||
},
|
},
|
||||||
ol({ node, ...props }) {
|
ol({ node, ...props }) {
|
||||||
return <ol className="max-w-full">{props.children}</ol>;
|
return <ol className="max-w-full">{props.children}</ol>;
|
||||||
},
|
},
|
||||||
|
strong({ node, ...props }) {
|
||||||
|
return <strong className="font-bold">{props.children}</strong>;
|
||||||
|
},
|
||||||
h1({ node, ...props }) {
|
h1({ node, ...props }) {
|
||||||
return <h1 className="mb-6 mt-4">{props.children}</h1>;
|
return <h1 className="mb-6 mt-4">{props.children}</h1>;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
10
frontend/package-lock.json
generated
10
frontend/package-lock.json
generated
|
|
@ -52,6 +52,7 @@
|
||||||
"sonner": "^2.0.6",
|
"sonner": "^2.0.6",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"use-stick-to-bottom": "^1.1.1",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -10224,6 +10225,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-stick-to-bottom": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-stick-to-bottom/-/use-stick-to-bottom-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-JkDp0b0tSmv7HQOOpL1hT7t7QaoUBXkq045WWWOFDTlLGRzgIIyW7vyzOIJzY7L2XVIG7j1yUxeDj2LHm9Vwng==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/use-sync-external-store": {
|
"node_modules/use-sync-external-store": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@
|
||||||
"sonner": "^2.0.6",
|
"sonner": "^2.0.6",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"use-stick-to-bottom": "^1.1.1",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -106,9 +106,8 @@ async function proxyRequest(
|
||||||
}
|
}
|
||||||
const response = await fetch(backendUrl, init);
|
const response = await fetch(backendUrl, init);
|
||||||
|
|
||||||
const responseBody = await response.text();
|
|
||||||
const responseHeaders = new Headers();
|
const responseHeaders = new Headers();
|
||||||
|
|
||||||
// Copy response headers
|
// Copy response headers
|
||||||
for (const [key, value] of response.headers.entries()) {
|
for (const [key, value] of response.headers.entries()) {
|
||||||
if (!key.toLowerCase().startsWith('transfer-encoding') &&
|
if (!key.toLowerCase().startsWith('transfer-encoding') &&
|
||||||
|
|
@ -117,11 +116,22 @@ async function proxyRequest(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new NextResponse(responseBody, {
|
// For streaming responses, pass the body directly without buffering
|
||||||
status: response.status,
|
if (response.body) {
|
||||||
statusText: response.statusText,
|
return new NextResponse(response.body, {
|
||||||
headers: responseHeaders,
|
status: response.status,
|
||||||
});
|
statusText: response.statusText,
|
||||||
|
headers: responseHeaders,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fallback for non-streaming responses
|
||||||
|
const responseBody = await response.text();
|
||||||
|
return new NextResponse(responseBody, {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
headers: responseHeaders,
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Proxy error:', error);
|
console.error('Proxy error:', error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|
|
||||||
|
|
@ -1,63 +1,87 @@
|
||||||
import { Bot, GitBranch } from "lucide-react";
|
import { GitBranch } from "lucide-react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import DogIcon from "@/components/logo/dog-icon";
|
||||||
import { MarkdownRenderer } from "@/components/markdown-renderer";
|
import { MarkdownRenderer } from "@/components/markdown-renderer";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { FunctionCall } from "../types";
|
||||||
import { FunctionCalls } from "./function-calls";
|
import { FunctionCalls } from "./function-calls";
|
||||||
import { Message } from "./message";
|
import { Message } from "./message";
|
||||||
import type { FunctionCall } from "../types";
|
|
||||||
import DogIcon from "@/components/logo/dog-icon";
|
|
||||||
|
|
||||||
interface AssistantMessageProps {
|
interface AssistantMessageProps {
|
||||||
content: string;
|
content: string;
|
||||||
functionCalls?: FunctionCall[];
|
functionCalls?: FunctionCall[];
|
||||||
messageIndex?: number;
|
messageIndex?: number;
|
||||||
expandedFunctionCalls: Set<string>;
|
expandedFunctionCalls: Set<string>;
|
||||||
onToggle: (functionCallId: string) => void;
|
onToggle: (functionCallId: string) => void;
|
||||||
isStreaming?: boolean;
|
isStreaming?: boolean;
|
||||||
showForkButton?: boolean;
|
showForkButton?: boolean;
|
||||||
onFork?: (e: React.MouseEvent) => void;
|
onFork?: (e: React.MouseEvent) => void;
|
||||||
|
isCompleted?: boolean;
|
||||||
|
animate?: boolean;
|
||||||
|
delay?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AssistantMessage({
|
export function AssistantMessage({
|
||||||
content,
|
content,
|
||||||
functionCalls = [],
|
functionCalls = [],
|
||||||
messageIndex,
|
messageIndex,
|
||||||
expandedFunctionCalls,
|
expandedFunctionCalls,
|
||||||
onToggle,
|
onToggle,
|
||||||
isStreaming = false,
|
isStreaming = false,
|
||||||
showForkButton = false,
|
showForkButton = false,
|
||||||
onFork,
|
onFork,
|
||||||
|
isCompleted = false,
|
||||||
|
animate = true,
|
||||||
|
delay = 0.2,
|
||||||
}: AssistantMessageProps) {
|
}: AssistantMessageProps) {
|
||||||
const updatedOnboarding = process.env.UPDATED_ONBOARDING === "true";
|
|
||||||
const IconComponent = updatedOnboarding ? DogIcon : Bot;
|
return (
|
||||||
|
<motion.div
|
||||||
return (
|
initial={animate ? { opacity: 0, y: -20 } : { opacity: 1, y: 0 }}
|
||||||
<Message
|
animate={{ opacity: 1, y: 0 }}
|
||||||
icon={
|
transition={animate ? { duration: 0.4, delay: delay, ease: "easeOut" } : { duration: 0 }}
|
||||||
<div className="w-8 h-8 rounded-lg bg-accent/20 flex items-center justify-center flex-shrink-0 select-none">
|
className={isCompleted ? "opacity-50" : ""}
|
||||||
<IconComponent className="h-4 w-4 text-accent-foreground" />
|
>
|
||||||
</div>
|
<Message
|
||||||
}
|
icon={
|
||||||
actions={
|
<div className="w-8 h-8 rounded-lg bg-accent/20 flex items-center justify-center flex-shrink-0 select-none">
|
||||||
showForkButton && onFork ? (
|
<DogIcon
|
||||||
<button
|
className="h-6 w-6 transition-colors duration-300"
|
||||||
onClick={onFork}
|
disabled={isCompleted}
|
||||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-accent rounded text-muted-foreground hover:text-foreground"
|
/>
|
||||||
title="Fork conversation from here"
|
</div>
|
||||||
>
|
}
|
||||||
<GitBranch className="h-3 w-3" />
|
actions={
|
||||||
</button>
|
showForkButton && onFork ? (
|
||||||
) : undefined
|
<button
|
||||||
}
|
type="button"
|
||||||
>
|
onClick={onFork}
|
||||||
<FunctionCalls
|
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-accent rounded text-muted-foreground hover:text-foreground"
|
||||||
functionCalls={functionCalls}
|
title="Fork conversation from here"
|
||||||
messageIndex={messageIndex}
|
>
|
||||||
expandedFunctionCalls={expandedFunctionCalls}
|
<GitBranch className="h-3 w-3" />
|
||||||
onToggle={onToggle}
|
</button>
|
||||||
/>
|
) : undefined
|
||||||
<MarkdownRenderer chatMessage={content} />
|
}
|
||||||
{isStreaming && (
|
>
|
||||||
<span className="inline-block w-2 h-4 bg-blue-400 ml-1 animate-pulse"></span>
|
<FunctionCalls
|
||||||
)}
|
functionCalls={functionCalls}
|
||||||
</Message>
|
messageIndex={messageIndex}
|
||||||
);
|
expandedFunctionCalls={expandedFunctionCalls}
|
||||||
|
onToggle={onToggle}
|
||||||
|
/>
|
||||||
|
<div className="relative">
|
||||||
|
<MarkdownRenderer
|
||||||
|
className={cn("text-sm py-1.5 transition-colors duration-300", isCompleted ? "text-placeholder-foreground" : "text-foreground")}
|
||||||
|
chatMessage={
|
||||||
|
isStreaming
|
||||||
|
? content +
|
||||||
|
' <span class="inline-block w-1 h-4 bg-primary ml-1 animate-pulse"></span>'
|
||||||
|
: content
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Message>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,282 +1,284 @@
|
||||||
import { Check, Funnel, Loader2, Plus, X } from "lucide-react";
|
import { Check, Funnel, Loader2, Plus, X } from "lucide-react";
|
||||||
import TextareaAutosize from "react-textarea-autosize";
|
|
||||||
import { forwardRef, useImperativeHandle, useRef } from "react";
|
import { forwardRef, useImperativeHandle, useRef } from "react";
|
||||||
|
import TextareaAutosize from "react-textarea-autosize";
|
||||||
|
import type { FilterColor } from "@/components/filter-icon-popover";
|
||||||
import { filterAccentClasses } from "@/components/knowledge-filter-panel";
|
import { filterAccentClasses } from "@/components/knowledge-filter-panel";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverAnchor,
|
PopoverAnchor,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
import type { KnowledgeFilterData } from "../types";
|
import type { KnowledgeFilterData } from "../types";
|
||||||
import { FilterColor } from "@/components/filter-icon-popover";
|
|
||||||
|
|
||||||
export interface ChatInputHandle {
|
export interface ChatInputHandle {
|
||||||
focusInput: () => void;
|
focusInput: () => void;
|
||||||
clickFileInput: () => void;
|
clickFileInput: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChatInputProps {
|
interface ChatInputProps {
|
||||||
input: string;
|
input: string;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
isUploading: boolean;
|
isUploading: boolean;
|
||||||
selectedFilter: KnowledgeFilterData | null;
|
selectedFilter: KnowledgeFilterData | null;
|
||||||
isFilterDropdownOpen: boolean;
|
isFilterDropdownOpen: boolean;
|
||||||
availableFilters: KnowledgeFilterData[];
|
availableFilters: KnowledgeFilterData[];
|
||||||
filterSearchTerm: string;
|
filterSearchTerm: string;
|
||||||
selectedFilterIndex: number;
|
selectedFilterIndex: number;
|
||||||
anchorPosition: { x: number; y: number } | null;
|
anchorPosition: { x: number; y: number } | null;
|
||||||
textareaHeight: number;
|
textareaHeight: number;
|
||||||
parsedFilterData: { color?: FilterColor } | null;
|
parsedFilterData: { color?: FilterColor } | null;
|
||||||
onSubmit: (e: React.FormEvent) => void;
|
onSubmit: (e: React.FormEvent) => void;
|
||||||
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||||
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||||
onHeightChange: (height: number) => void;
|
onHeightChange: (height: number) => void;
|
||||||
onFilterSelect: (filter: KnowledgeFilterData | null) => void;
|
onFilterSelect: (filter: KnowledgeFilterData | null) => void;
|
||||||
onAtClick: () => void;
|
onAtClick: () => void;
|
||||||
onFilePickerChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
onFilePickerChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
onFilePickerClick: () => void;
|
onFilePickerClick: () => void;
|
||||||
setSelectedFilter: (filter: KnowledgeFilterData | null) => void;
|
setSelectedFilter: (filter: KnowledgeFilterData | null) => void;
|
||||||
setIsFilterHighlighted: (highlighted: boolean) => void;
|
setIsFilterHighlighted: (highlighted: boolean) => void;
|
||||||
setIsFilterDropdownOpen: (open: boolean) => void;
|
setIsFilterDropdownOpen: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>((
|
export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
||||||
{
|
(
|
||||||
input,
|
{
|
||||||
loading,
|
input,
|
||||||
isUploading,
|
loading,
|
||||||
selectedFilter,
|
isUploading,
|
||||||
isFilterDropdownOpen,
|
selectedFilter,
|
||||||
availableFilters,
|
isFilterDropdownOpen,
|
||||||
filterSearchTerm,
|
availableFilters,
|
||||||
selectedFilterIndex,
|
filterSearchTerm,
|
||||||
anchorPosition,
|
selectedFilterIndex,
|
||||||
textareaHeight,
|
anchorPosition,
|
||||||
parsedFilterData,
|
textareaHeight,
|
||||||
onSubmit,
|
parsedFilterData,
|
||||||
onChange,
|
onSubmit,
|
||||||
onKeyDown,
|
onChange,
|
||||||
onHeightChange,
|
onKeyDown,
|
||||||
onFilterSelect,
|
onHeightChange,
|
||||||
onAtClick,
|
onFilterSelect,
|
||||||
onFilePickerChange,
|
onAtClick,
|
||||||
onFilePickerClick,
|
onFilePickerChange,
|
||||||
setSelectedFilter,
|
onFilePickerClick,
|
||||||
setIsFilterHighlighted,
|
setSelectedFilter,
|
||||||
setIsFilterDropdownOpen,
|
setIsFilterHighlighted,
|
||||||
},
|
setIsFilterDropdownOpen,
|
||||||
ref
|
},
|
||||||
) => {
|
ref,
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
) => {
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
focusInput: () => {
|
focusInput: () => {
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
},
|
},
|
||||||
clickFileInput: () => {
|
clickFileInput: () => {
|
||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pb-8 pt-4 flex px-6">
|
<div className="pb-8 flex px-6">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<form onSubmit={onSubmit} className="relative">
|
<form onSubmit={onSubmit} className="relative">
|
||||||
<div className="relative w-full bg-muted/20 rounded-lg border border-border/50 focus-within:ring-1 focus-within:ring-ring">
|
<div className="relative w-full bg-muted/20 rounded-lg border border-border/50 focus-within:ring-1 focus-within:ring-ring">
|
||||||
{selectedFilter && (
|
{selectedFilter && (
|
||||||
<div className="flex items-center gap-2 px-4 pt-3 pb-1">
|
<div className="flex items-center gap-2 px-4 pt-3 pb-1">
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium transition-colors ${
|
className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium transition-colors ${
|
||||||
filterAccentClasses[parsedFilterData?.color || "zinc"]
|
filterAccentClasses[parsedFilterData?.color || "zinc"]
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@filter:{selectedFilter.name}
|
@filter:{selectedFilter.name}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedFilter(null);
|
setSelectedFilter(null);
|
||||||
setIsFilterHighlighted(false);
|
setIsFilterHighlighted(false);
|
||||||
}}
|
}}
|
||||||
className="ml-1 rounded-full p-0.5"
|
className="ml-1 rounded-full p-0.5"
|
||||||
>
|
>
|
||||||
<X className="h-3 w-3" />
|
<X className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className="relative"
|
className="relative"
|
||||||
style={{ height: `${textareaHeight + 60}px` }}
|
style={{ height: `${textareaHeight + 60}px` }}
|
||||||
>
|
>
|
||||||
<TextareaAutosize
|
<TextareaAutosize
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
value={input}
|
value={input}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
onHeightChange={onHeightChange}
|
onHeightChange={onHeightChange}
|
||||||
maxRows={7}
|
maxRows={7}
|
||||||
minRows={2}
|
minRows={2}
|
||||||
placeholder="Type to ask a question..."
|
placeholder="Type to ask a question..."
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className={`w-full bg-transparent px-4 ${
|
className={`w-full bg-transparent px-4 ${
|
||||||
selectedFilter ? "pt-2" : "pt-4"
|
selectedFilter ? "pt-2" : "pt-4"
|
||||||
} focus-visible:outline-none resize-none`}
|
} focus-visible:outline-none resize-none`}
|
||||||
rows={2}
|
rows={2}
|
||||||
/>
|
/>
|
||||||
{/* Safe area at bottom for buttons */}
|
{/* Safe area at bottom for buttons */}
|
||||||
<div
|
<div
|
||||||
className="absolute bottom-0 left-0 right-0 bg-transparent pointer-events-none"
|
className="absolute bottom-0 left-0 right-0 bg-transparent pointer-events-none"
|
||||||
style={{ height: "60px" }}
|
style={{ height: "60px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
onChange={onFilePickerChange}
|
onChange={onFilePickerChange}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
accept=".pdf,.doc,.docx,.txt,.md,.rtf,.odt"
|
accept=".pdf,.doc,.docx,.txt,.md,.rtf,.odt"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="iconSm"
|
size="iconSm"
|
||||||
className="absolute bottom-3 left-3 h-8 w-8 p-0 rounded-full hover:bg-muted/50"
|
className="absolute bottom-3 left-3 h-8 w-8 p-0 rounded-full hover:bg-muted/50"
|
||||||
onMouseDown={e => {
|
onMouseDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}}
|
}}
|
||||||
onClick={onAtClick}
|
onClick={onAtClick}
|
||||||
data-filter-button
|
data-filter-button
|
||||||
>
|
>
|
||||||
<Funnel className="h-4 w-4" />
|
<Funnel className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Popover
|
<Popover
|
||||||
open={isFilterDropdownOpen}
|
open={isFilterDropdownOpen}
|
||||||
onOpenChange={open => {
|
onOpenChange={(open) => {
|
||||||
setIsFilterDropdownOpen(open);
|
setIsFilterDropdownOpen(open);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{anchorPosition && (
|
{anchorPosition && (
|
||||||
<PopoverAnchor
|
<PopoverAnchor
|
||||||
asChild
|
asChild
|
||||||
style={{
|
style={{
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
left: anchorPosition.x,
|
left: anchorPosition.x,
|
||||||
top: anchorPosition.y,
|
top: anchorPosition.y,
|
||||||
width: 1,
|
width: 1,
|
||||||
height: 1,
|
height: 1,
|
||||||
pointerEvents: "none",
|
pointerEvents: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div />
|
<div />
|
||||||
</PopoverAnchor>
|
</PopoverAnchor>
|
||||||
)}
|
)}
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
className="w-64 p-2"
|
className="w-64 p-2"
|
||||||
side="top"
|
side="top"
|
||||||
align="start"
|
align="start"
|
||||||
sideOffset={6}
|
sideOffset={6}
|
||||||
alignOffset={-18}
|
alignOffset={-18}
|
||||||
onOpenAutoFocus={e => {
|
onOpenAutoFocus={(e) => {
|
||||||
// Prevent auto focus on the popover content
|
// Prevent auto focus on the popover content
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// Keep focus on the input
|
// Keep focus on the input
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{filterSearchTerm && (
|
{filterSearchTerm && (
|
||||||
<div className="px-2 py-1.5 text-xs font-medium text-muted-foreground">
|
<div className="px-2 py-1.5 text-xs font-medium text-muted-foreground">
|
||||||
Searching: @{filterSearchTerm}
|
Searching: @{filterSearchTerm}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{availableFilters.length === 0 ? (
|
{availableFilters.length === 0 ? (
|
||||||
<div className="px-2 py-3 text-sm text-muted-foreground">
|
<div className="px-2 py-3 text-sm text-muted-foreground">
|
||||||
No knowledge filters available
|
No knowledge filters available
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{!filterSearchTerm && (
|
{!filterSearchTerm && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onFilterSelect(null)}
|
onClick={() => onFilterSelect(null)}
|
||||||
className={`w-full text-left px-2 py-2 text-sm rounded hover:bg-muted/50 flex items-center justify-between ${
|
className={`w-full text-left px-2 py-2 text-sm rounded hover:bg-muted/50 flex items-center justify-between ${
|
||||||
selectedFilterIndex === -1 ? "bg-muted/50" : ""
|
selectedFilterIndex === -1 ? "bg-muted/50" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span>No knowledge filter</span>
|
<span>No knowledge filter</span>
|
||||||
{!selectedFilter && (
|
{!selectedFilter && (
|
||||||
<Check className="h-4 w-4 shrink-0" />
|
<Check className="h-4 w-4 shrink-0" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{availableFilters
|
{availableFilters
|
||||||
.filter(filter =>
|
.filter((filter) =>
|
||||||
filter.name
|
filter.name
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(filterSearchTerm.toLowerCase())
|
.includes(filterSearchTerm.toLowerCase()),
|
||||||
)
|
)
|
||||||
.map((filter, index) => (
|
.map((filter, index) => (
|
||||||
<button
|
<button
|
||||||
key={filter.id}
|
key={filter.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onFilterSelect(filter)}
|
onClick={() => onFilterSelect(filter)}
|
||||||
className={`w-full overflow-hidden text-left px-2 py-2 gap-2 text-sm rounded hover:bg-muted/50 flex items-center justify-between ${
|
className={`w-full overflow-hidden text-left px-2 py-2 gap-2 text-sm rounded hover:bg-muted/50 flex items-center justify-between ${
|
||||||
index === selectedFilterIndex ? "bg-muted/50" : ""
|
index === selectedFilterIndex ? "bg-muted/50" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
<div className="font-medium truncate">
|
<div className="font-medium truncate">
|
||||||
{filter.name}
|
{filter.name}
|
||||||
</div>
|
</div>
|
||||||
{filter.description && (
|
{filter.description && (
|
||||||
<div className="text-xs text-muted-foreground truncate">
|
<div className="text-xs text-muted-foreground truncate">
|
||||||
{filter.description}
|
{filter.description}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{selectedFilter?.id === filter.id && (
|
{selectedFilter?.id === filter.id && (
|
||||||
<Check className="h-4 w-4 shrink-0" />
|
<Check className="h-4 w-4 shrink-0" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
{availableFilters.filter(filter =>
|
{availableFilters.filter((filter) =>
|
||||||
filter.name
|
filter.name
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(filterSearchTerm.toLowerCase())
|
.includes(filterSearchTerm.toLowerCase()),
|
||||||
).length === 0 &&
|
).length === 0 &&
|
||||||
filterSearchTerm && (
|
filterSearchTerm && (
|
||||||
<div className="px-2 py-3 text-sm text-muted-foreground">
|
<div className="px-2 py-3 text-sm text-muted-foreground">
|
||||||
No filters match "{filterSearchTerm}"
|
No filters match "{filterSearchTerm}"
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="iconSm"
|
size="iconSm"
|
||||||
onClick={onFilePickerClick}
|
onClick={onFilePickerClick}
|
||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
className="absolute bottom-3 left-12 h-8 w-8 p-0 rounded-full hover:bg-muted/50"
|
className="absolute bottom-3 left-12 h-8 w-8 p-0 rounded-full hover:bg-muted/50"
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!input.trim() || loading}
|
disabled={!input.trim() || loading}
|
||||||
className="absolute bottom-3 right-3 rounded-lg h-10 px-4"
|
className="absolute bottom-3 right-3 rounded-lg h-10 px-4"
|
||||||
>
|
>
|
||||||
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : "Send"}
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : "Send"}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
ChatInput.displayName = "ChatInput";
|
ChatInput.displayName = "ChatInput";
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,52 @@
|
||||||
import { User } from "lucide-react";
|
import { User } from "lucide-react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { useAuth } from "@/contexts/auth-context";
|
import { useAuth } from "@/contexts/auth-context";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { Message } from "./message";
|
import { Message } from "./message";
|
||||||
|
|
||||||
interface UserMessageProps {
|
interface UserMessageProps {
|
||||||
content: string;
|
content: string;
|
||||||
|
isCompleted?: boolean;
|
||||||
|
animate?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserMessage({ content }: UserMessageProps) {
|
export function UserMessage({ content, isCompleted, animate = true }: UserMessageProps) {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
return (
|
console.log("animate", animate);
|
||||||
<Message
|
|
||||||
icon={
|
return (
|
||||||
<Avatar className="w-8 h-8 flex-shrink-0 select-none">
|
<motion.div
|
||||||
<AvatarImage draggable={false} src={user?.picture} alt={user?.name} />
|
initial={animate ? { opacity: 0, y: -20 } : { opacity: 1, y: 0 }}
|
||||||
<AvatarFallback className="text-sm bg-primary/20 text-primary">
|
animate={{ opacity: 1, y: 0 }}
|
||||||
{user?.name ? (
|
transition={animate ? { duration: 0.4, delay: 0.2, ease: "easeOut" } : { duration: 0 }}
|
||||||
user.name.charAt(0).toUpperCase()
|
className={isCompleted ? "opacity-50" : ""}
|
||||||
) : (
|
>
|
||||||
<User className="h-4 w-4" />
|
<Message
|
||||||
)}
|
icon={
|
||||||
</AvatarFallback>
|
<Avatar className="w-8 h-8 rounded-lg flex-shrink-0 select-none">
|
||||||
</Avatar>
|
<AvatarImage draggable={false} src={user?.picture} alt={user?.name} />
|
||||||
}
|
<AvatarFallback
|
||||||
>
|
className={cn(
|
||||||
<p className="text-foreground whitespace-pre-wrap break-words overflow-wrap-anywhere">
|
isCompleted ? "text-placeholder-foreground" : "text-primary",
|
||||||
{content}
|
"text-sm bg-accent/20 rounded-lg transition-colors duration-300",
|
||||||
</p>
|
)}
|
||||||
</Message>
|
>
|
||||||
);
|
{user?.name ? user.name.charAt(0).toUpperCase() : <User className="h-4 w-4" />}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
"text-foreground text-sm py-1.5 whitespace-pre-wrap break-words overflow-wrap-anywhere transition-colors duration-300",
|
||||||
|
isCompleted ? "text-placeholder-foreground" : "text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</p>
|
||||||
|
</Message>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,55 @@
|
||||||
import { motion, AnimatePresence } from "motion/react";
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export default function Nudges({
|
export default function Nudges({
|
||||||
nudges,
|
nudges,
|
||||||
handleSuggestionClick,
|
onboarding,
|
||||||
|
handleSuggestionClick,
|
||||||
}: {
|
}: {
|
||||||
nudges: string[];
|
nudges: string[];
|
||||||
|
onboarding?: boolean;
|
||||||
handleSuggestionClick: (suggestion: string) => void;
|
handleSuggestionClick: (suggestion: string) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex-shrink-0 h-12 w-full overflow-hidden">
|
<div className="flex-shrink-0 h-12 w-full overflow-hidden">
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{nudges.length > 0 && (
|
{nudges.length > 0 && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, y: 20 }}
|
exit={{ opacity: 0, y: 20 }}
|
||||||
transition={{
|
transition={{
|
||||||
duration: 0.2,
|
duration: 0.2,
|
||||||
ease: "easeInOut",
|
ease: "easeInOut",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="relative px-6 pt-4 flex justify-center">
|
<div
|
||||||
<div className="w-full max-w-[75%]">
|
className="relative flex"
|
||||||
<div className="flex gap-3 justify-start overflow-x-auto scrollbar-hide">
|
>
|
||||||
{nudges.map((suggestion: string, index: number) => (
|
<div className="w-full">
|
||||||
<button
|
<div className="flex gap-3 justify-start overflow-x-auto scrollbar-hide">
|
||||||
key={index}
|
{nudges.map((suggestion: string, index: number) => (
|
||||||
onClick={() => handleSuggestionClick(suggestion)}
|
<button
|
||||||
className="px-2 py-1.5 bg-muted hover:bg-muted/50 rounded-lg text-sm text-placeholder-foreground hover:text-foreground transition-colors whitespace-nowrap"
|
key={index}
|
||||||
>
|
onClick={() => handleSuggestionClick(suggestion)}
|
||||||
{suggestion}
|
className={cn(
|
||||||
</button>
|
onboarding
|
||||||
))}
|
? "text-foreground"
|
||||||
</div>
|
: "text-placeholder-foreground hover:text-foreground",
|
||||||
{/* Fade out gradient on the right */}
|
"bg-background border hover:bg-background/50 px-2 py-1.5 rounded-lg text-sm transition-colors whitespace-nowrap",
|
||||||
<div className="absolute right-0 top-0 bottom-0 w-8 bg-gradient-to-l from-background to-transparent pointer-events-none"></div>
|
)}
|
||||||
</div>
|
>
|
||||||
</div>
|
{suggestion}
|
||||||
</motion.div>
|
</button>
|
||||||
)}
|
))}
|
||||||
</AnimatePresence>
|
</div>
|
||||||
</div>
|
{/* Fade out gradient on the right */}
|
||||||
);
|
<div className="absolute right-0 top-0 bottom-0 w-8 bg-gradient-to-l from-background to-transparent pointer-events-none"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -4,6 +4,7 @@ export interface Message {
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
functionCalls?: FunctionCall[];
|
functionCalls?: FunctionCall[];
|
||||||
isStreaming?: boolean;
|
isStreaming?: boolean;
|
||||||
|
source?: "langflow" | "chat";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FunctionCall {
|
export interface FunctionCall {
|
||||||
|
|
|
||||||
|
|
@ -345,6 +345,15 @@
|
||||||
@apply text-xs opacity-70;
|
@apply text-xs opacity-70;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prose :where(strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
|
||||||
|
@apply text-current;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(a):not(:where([class~="not-prose"],[class~="not-prose"] *))
|
||||||
|
{
|
||||||
|
@apply text-current;
|
||||||
|
}
|
||||||
|
|
||||||
.box-shadow-inner::after {
|
.box-shadow-inner::after {
|
||||||
content: " ";
|
content: " ";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
||||||
|
|
@ -1,78 +1,168 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { StickToBottom } from "use-stick-to-bottom";
|
||||||
|
import { AssistantMessage } from "@/app/chat/components/assistant-message";
|
||||||
|
import { UserMessage } from "@/app/chat/components/user-message";
|
||||||
|
import Nudges from "@/app/chat/nudges";
|
||||||
|
import type { Message } from "@/app/chat/types";
|
||||||
import OnboardingCard from "@/app/onboarding/components/onboarding-card";
|
import OnboardingCard from "@/app/onboarding/components/onboarding-card";
|
||||||
|
import { useChatStreaming } from "@/hooks/useChatStreaming";
|
||||||
import { OnboardingStep } from "./onboarding-step";
|
import { OnboardingStep } from "./onboarding-step";
|
||||||
|
|
||||||
export function OnboardingContent({
|
export function OnboardingContent({
|
||||||
handleStepComplete,
|
handleStepComplete,
|
||||||
currentStep,
|
currentStep,
|
||||||
}: {
|
}: {
|
||||||
handleStepComplete: () => void;
|
handleStepComplete: () => void;
|
||||||
currentStep: number;
|
currentStep: number;
|
||||||
}) {
|
}) {
|
||||||
return (
|
const [responseId, setResponseId] = useState<string | null>(null);
|
||||||
<div className="space-y-6">
|
const [selectedNudge, setSelectedNudge] = useState<string>("");
|
||||||
<OnboardingStep
|
const [assistantMessage, setAssistantMessage] = useState<Message | null>(
|
||||||
isVisible={currentStep >= 0}
|
null,
|
||||||
isCompleted={currentStep > 0}
|
);
|
||||||
text="Let's get started by setting up your model provider."
|
|
||||||
>
|
|
||||||
<OnboardingCard onComplete={handleStepComplete} />
|
|
||||||
</OnboardingStep>
|
|
||||||
|
|
||||||
<OnboardingStep
|
const { streamingMessage, isLoading, sendMessage } = useChatStreaming({
|
||||||
isVisible={currentStep >= 1}
|
onComplete: (message, newResponseId) => {
|
||||||
isCompleted={currentStep > 1}
|
setAssistantMessage(message);
|
||||||
text="Step 1: Configure your settings"
|
if (newResponseId) {
|
||||||
>
|
setResponseId(newResponseId);
|
||||||
<div className="space-y-4">
|
}
|
||||||
<p className="text-muted-foreground">
|
},
|
||||||
Let's configure some basic settings for your account.
|
onError: (error) => {
|
||||||
</p>
|
console.error("Chat error:", error);
|
||||||
<button
|
setAssistantMessage({
|
||||||
onClick={handleStepComplete}
|
role: "assistant",
|
||||||
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90"
|
content:
|
||||||
>
|
"Sorry, I couldn't connect to the chat service. Please try again.",
|
||||||
Continue
|
timestamp: new Date(),
|
||||||
</button>
|
});
|
||||||
</div>
|
},
|
||||||
</OnboardingStep>
|
});
|
||||||
|
|
||||||
<OnboardingStep
|
const NUDGES = ["What is OpenRAG?"];
|
||||||
isVisible={currentStep >= 2}
|
|
||||||
isCompleted={currentStep > 2}
|
|
||||||
text="Step 2: Connect your model"
|
|
||||||
>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Choose and connect your preferred AI model provider.
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={handleStepComplete}
|
|
||||||
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90"
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</OnboardingStep>
|
|
||||||
|
|
||||||
<OnboardingStep
|
const handleNudgeClick = async (nudge: string) => {
|
||||||
isVisible={currentStep >= 3}
|
setSelectedNudge(nudge);
|
||||||
isCompleted={currentStep > 3}
|
setAssistantMessage(null);
|
||||||
text="Step 3: You're all set!"
|
setTimeout(async () => {
|
||||||
>
|
await sendMessage({
|
||||||
<div className="space-y-4">
|
prompt: nudge,
|
||||||
<p className="text-muted-foreground">
|
previousResponseId: responseId || undefined,
|
||||||
Your account is ready to use. Let's start chatting!
|
});
|
||||||
</p>
|
}, 1500);
|
||||||
<button
|
};
|
||||||
onClick={handleStepComplete}
|
|
||||||
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90"
|
// Determine which message to show (streaming takes precedence)
|
||||||
>
|
const displayMessage = streamingMessage || assistantMessage;
|
||||||
Go to Chat
|
|
||||||
</button>
|
return (
|
||||||
</div>
|
<StickToBottom
|
||||||
</OnboardingStep>
|
className="flex h-full flex-1 flex-col"
|
||||||
</div>
|
resize="smooth"
|
||||||
);
|
initial="instant"
|
||||||
|
mass={1}
|
||||||
|
>
|
||||||
|
<StickToBottom.Content className="flex flex-col min-h-full overflow-x-hidden px-8 py-6">
|
||||||
|
<div className="flex flex-col place-self-center w-full space-y-6">
|
||||||
|
<OnboardingStep
|
||||||
|
isVisible={currentStep >= 0}
|
||||||
|
isCompleted={currentStep > 0}
|
||||||
|
text="Let's get started by setting up your model provider."
|
||||||
|
>
|
||||||
|
<OnboardingCard onComplete={handleStepComplete} />
|
||||||
|
</OnboardingStep>
|
||||||
|
|
||||||
|
<OnboardingStep
|
||||||
|
isVisible={currentStep >= 1}
|
||||||
|
isCompleted={currentStep > 1 || !!selectedNudge}
|
||||||
|
text="Excellent, let's move on to learning the basics."
|
||||||
|
>
|
||||||
|
<div className="py-2">
|
||||||
|
<Nudges
|
||||||
|
onboarding
|
||||||
|
nudges={NUDGES}
|
||||||
|
handleSuggestionClick={handleNudgeClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</OnboardingStep>
|
||||||
|
|
||||||
|
{/* User message - show when nudge is selected */}
|
||||||
|
{currentStep >= 1 && !!selectedNudge && (
|
||||||
|
<UserMessage
|
||||||
|
content={selectedNudge}
|
||||||
|
isCompleted={currentStep > 1}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Assistant message - show streaming or final message */}
|
||||||
|
{currentStep >= 1 &&
|
||||||
|
!!selectedNudge &&
|
||||||
|
(displayMessage || isLoading) && (
|
||||||
|
<>
|
||||||
|
<AssistantMessage
|
||||||
|
content={displayMessage?.content || ""}
|
||||||
|
functionCalls={displayMessage?.functionCalls}
|
||||||
|
messageIndex={0}
|
||||||
|
expandedFunctionCalls={new Set()}
|
||||||
|
onToggle={() => {}}
|
||||||
|
isStreaming={!!streamingMessage}
|
||||||
|
isCompleted={currentStep > 1}
|
||||||
|
/>
|
||||||
|
{!isLoading && displayMessage && currentStep === 1 && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleStepComplete}
|
||||||
|
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90"
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<OnboardingStep
|
||||||
|
isVisible={currentStep >= 2}
|
||||||
|
isCompleted={currentStep > 2}
|
||||||
|
text="Step 2: Connect your model"
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Choose and connect your preferred AI model provider.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleStepComplete}
|
||||||
|
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90"
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</OnboardingStep>
|
||||||
|
|
||||||
|
<OnboardingStep
|
||||||
|
isVisible={currentStep >= 3}
|
||||||
|
isCompleted={currentStep > 3}
|
||||||
|
text="Step 3: You're all set!"
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Your account is ready to use. Let's start chatting!
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleStepComplete}
|
||||||
|
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90"
|
||||||
|
>
|
||||||
|
Go to Chat
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</OnboardingStep>
|
||||||
|
</div>
|
||||||
|
</StickToBottom.Content>
|
||||||
|
</StickToBottom>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,89 +2,120 @@ import { AnimatePresence, motion } from "motion/react";
|
||||||
import { type ReactNode, useEffect, useState } from "react";
|
import { type ReactNode, useEffect, useState } from "react";
|
||||||
import { Message } from "@/app/chat/components/message";
|
import { Message } from "@/app/chat/components/message";
|
||||||
import DogIcon from "@/components/logo/dog-icon";
|
import DogIcon from "@/components/logo/dog-icon";
|
||||||
|
import { MarkdownRenderer } from "@/components/markdown-renderer";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface OnboardingStepProps {
|
interface OnboardingStepProps {
|
||||||
text: string;
|
text: string;
|
||||||
children: ReactNode;
|
children?: ReactNode;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
isCompleted?: boolean;
|
isCompleted?: boolean;
|
||||||
|
icon?: ReactNode;
|
||||||
|
isMarkdown?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OnboardingStep({
|
export function OnboardingStep({
|
||||||
text,
|
text,
|
||||||
children,
|
children,
|
||||||
isVisible,
|
isVisible,
|
||||||
isCompleted = false,
|
isCompleted = false,
|
||||||
|
icon,
|
||||||
|
isMarkdown = false,
|
||||||
}: OnboardingStepProps) {
|
}: OnboardingStepProps) {
|
||||||
const [displayedText, setDisplayedText] = useState("");
|
const [displayedText, setDisplayedText] = useState("");
|
||||||
const [showChildren, setShowChildren] = useState(false);
|
const [showChildren, setShowChildren] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isVisible) {
|
if (!isVisible) {
|
||||||
setDisplayedText("");
|
setDisplayedText("");
|
||||||
setShowChildren(false);
|
setShowChildren(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentIndex = 0;
|
if (isCompleted) {
|
||||||
setDisplayedText("");
|
setDisplayedText(text);
|
||||||
setShowChildren(false);
|
setShowChildren(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
let currentIndex = 0;
|
||||||
if (currentIndex < text.length) {
|
setDisplayedText("");
|
||||||
setDisplayedText(text.slice(0, currentIndex + 1));
|
setShowChildren(false);
|
||||||
currentIndex++;
|
|
||||||
} else {
|
|
||||||
clearInterval(interval);
|
|
||||||
setShowChildren(true);
|
|
||||||
}
|
|
||||||
}, 20); // 20ms per character
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
const interval = setInterval(() => {
|
||||||
}, [text, isVisible]);
|
if (currentIndex < text.length) {
|
||||||
|
setDisplayedText(text.slice(0, currentIndex + 1));
|
||||||
|
currentIndex++;
|
||||||
|
} else {
|
||||||
|
clearInterval(interval);
|
||||||
|
setShowChildren(true);
|
||||||
|
}
|
||||||
|
}, 20); // 20ms per character
|
||||||
|
|
||||||
if (!isVisible) return null;
|
return () => clearInterval(interval);
|
||||||
|
}, [text, isVisible, isCompleted]);
|
||||||
|
|
||||||
return (
|
if (!isVisible) return null;
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: -20 }}
|
return (
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<motion.div
|
||||||
transition={{ duration: 0.4, delay: 0.4, ease: "easeOut" }}
|
initial={{ opacity: 0, y: -20 }}
|
||||||
className={isCompleted ? "opacity-50" : ""}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
>
|
transition={{ duration: 0.4, delay: 0.4, ease: "easeOut" }}
|
||||||
<Message
|
className={isCompleted ? "opacity-50" : ""}
|
||||||
icon={
|
>
|
||||||
<div className="w-8 h-8 rounded-lg bg-accent/20 flex items-center justify-center flex-shrink-0 select-none">
|
<Message
|
||||||
<DogIcon
|
icon={
|
||||||
className="h-6 w-6 text-accent-foreground"
|
icon || (
|
||||||
disabled={isCompleted}
|
<div className="w-8 h-8 rounded-lg bg-accent/20 flex items-center justify-center flex-shrink-0 select-none">
|
||||||
/>
|
<DogIcon
|
||||||
</div>
|
className="h-6 w-6 text-accent-foreground transition-colors duration-300"
|
||||||
}
|
disabled={isCompleted}
|
||||||
>
|
/>
|
||||||
<div className="space-y-4">
|
</div>
|
||||||
<p
|
)
|
||||||
className={`text-foreground text-sm py-1.5 ${isCompleted ? "text-placeholder-foreground" : ""}`}
|
}
|
||||||
>
|
>
|
||||||
{displayedText}
|
<div>
|
||||||
{!showChildren && !isCompleted && (
|
{isMarkdown ? (
|
||||||
<span className="inline-block w-1 h-4 bg-primary ml-1 animate-pulse" />
|
<MarkdownRenderer
|
||||||
)}
|
className={cn(
|
||||||
</p>
|
isCompleted
|
||||||
<AnimatePresence>
|
? "text-placeholder-foreground"
|
||||||
{showChildren && !isCompleted && (
|
: "text-foreground",
|
||||||
<motion.div
|
"text-sm py-1.5 transition-colors duration-300",
|
||||||
initial={{ opacity: 0, y: -10 }}
|
)}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
chatMessage={text}
|
||||||
exit={{ opacity: 0, height: 0 }}
|
/>
|
||||||
transition={{ duration: 0.3, delay: 0.3, ease: "easeOut" }}
|
) : (
|
||||||
>
|
<p
|
||||||
{children}
|
className={`text-foreground text-sm py-1.5 transition-colors duration-300 ${
|
||||||
</motion.div>
|
isCompleted ? "text-placeholder-foreground" : ""
|
||||||
)}
|
}`}
|
||||||
</AnimatePresence>
|
>
|
||||||
</div>
|
{displayedText}
|
||||||
</Message>
|
{!showChildren && !isCompleted && (
|
||||||
</motion.div>
|
<span className="inline-block w-1 h-3.5 bg-primary ml-1 animate-pulse" />
|
||||||
);
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{children && (
|
||||||
|
<AnimatePresence>
|
||||||
|
{((showChildren && !isCompleted) || isMarkdown) && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, height: 0 }}
|
||||||
|
transition={{ duration: 0.3, delay: 0.3, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
<div className="pt-2">
|
||||||
|
{children}</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Message>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ import { motion } from "framer-motion";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
type ChatConversation,
|
type ChatConversation,
|
||||||
useGetConversationsQuery,
|
useGetConversationsQuery,
|
||||||
} from "@/app/api/queries/useGetConversationsQuery";
|
} from "@/app/api/queries/useGetConversationsQuery";
|
||||||
import type { Settings } from "@/app/api/queries/useGetSettingsQuery";
|
import type { Settings } from "@/app/api/queries/useGetSettingsQuery";
|
||||||
import { OnboardingContent } from "@/app/new-onboarding/components/onboarding-content";
|
import { OnboardingContent } from "@/app/new-onboarding/components/onboarding-content";
|
||||||
|
|
@ -16,187 +16,187 @@ import { Navigation } from "@/components/navigation";
|
||||||
import { useAuth } from "@/contexts/auth-context";
|
import { useAuth } from "@/contexts/auth-context";
|
||||||
import { useChat } from "@/contexts/chat-context";
|
import { useChat } from "@/contexts/chat-context";
|
||||||
import {
|
import {
|
||||||
ANIMATION_DURATION,
|
ANIMATION_DURATION,
|
||||||
HEADER_HEIGHT,
|
HEADER_HEIGHT,
|
||||||
ONBOARDING_STEP_KEY,
|
ONBOARDING_STEP_KEY,
|
||||||
SIDEBAR_WIDTH,
|
SIDEBAR_WIDTH,
|
||||||
TOTAL_ONBOARDING_STEPS,
|
TOTAL_ONBOARDING_STEPS,
|
||||||
} from "@/lib/constants";
|
} from "@/lib/constants";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export function ChatRenderer({
|
export function ChatRenderer({
|
||||||
settings,
|
settings,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { isAuthenticated, isNoAuthMode } = useAuth();
|
const { isAuthenticated, isNoAuthMode } = useAuth();
|
||||||
const {
|
const {
|
||||||
endpoint,
|
endpoint,
|
||||||
refreshTrigger,
|
refreshTrigger,
|
||||||
refreshConversations,
|
refreshConversations,
|
||||||
startNewConversation,
|
startNewConversation,
|
||||||
} = useChat();
|
} = useChat();
|
||||||
|
|
||||||
// Initialize onboarding state based on local storage and settings
|
// Initialize onboarding state based on local storage and settings
|
||||||
const [currentStep, setCurrentStep] = useState<number>(() => {
|
const [currentStep, setCurrentStep] = useState<number>(() => {
|
||||||
if (typeof window === "undefined") return 0;
|
if (typeof window === "undefined") return 0;
|
||||||
const savedStep = localStorage.getItem(ONBOARDING_STEP_KEY);
|
const savedStep = localStorage.getItem(ONBOARDING_STEP_KEY);
|
||||||
return savedStep !== null ? parseInt(savedStep, 10) : 0;
|
return savedStep !== null ? parseInt(savedStep, 10) : 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
const [showLayout, setShowLayout] = useState<boolean>(() => {
|
const [showLayout, setShowLayout] = useState<boolean>(() => {
|
||||||
if (typeof window === "undefined") return false;
|
if (typeof window === "undefined") return false;
|
||||||
const savedStep = localStorage.getItem(ONBOARDING_STEP_KEY);
|
const savedStep = localStorage.getItem(ONBOARDING_STEP_KEY);
|
||||||
// Show layout if settings.edited is true and if no onboarding step is saved
|
// Show layout if settings.edited is true and if no onboarding step is saved
|
||||||
return !!settings?.edited && savedStep === null;
|
return !!settings?.edited && savedStep === null;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Only fetch conversations on chat page
|
// Only fetch conversations on chat page
|
||||||
const isOnChatPage = pathname === "/" || pathname === "/chat";
|
const isOnChatPage = pathname === "/" || pathname === "/chat";
|
||||||
const { data: conversations = [], isLoading: isConversationsLoading } =
|
const { data: conversations = [], isLoading: isConversationsLoading } =
|
||||||
useGetConversationsQuery(endpoint, refreshTrigger, {
|
useGetConversationsQuery(endpoint, refreshTrigger, {
|
||||||
enabled: isOnChatPage && (isAuthenticated || isNoAuthMode),
|
enabled: isOnChatPage && (isAuthenticated || isNoAuthMode),
|
||||||
}) as { data: ChatConversation[]; isLoading: boolean };
|
}) as { data: ChatConversation[]; isLoading: boolean };
|
||||||
|
|
||||||
const handleNewConversation = () => {
|
const handleNewConversation = () => {
|
||||||
refreshConversations();
|
refreshConversations();
|
||||||
startNewConversation();
|
startNewConversation();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save current step to local storage whenever it changes
|
// Save current step to local storage whenever it changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== "undefined" && !showLayout) {
|
if (typeof window !== "undefined" && !showLayout) {
|
||||||
localStorage.setItem(ONBOARDING_STEP_KEY, currentStep.toString());
|
localStorage.setItem(ONBOARDING_STEP_KEY, currentStep.toString());
|
||||||
}
|
}
|
||||||
}, [currentStep, showLayout]);
|
}, [currentStep, showLayout]);
|
||||||
|
|
||||||
const handleStepComplete = () => {
|
const handleStepComplete = () => {
|
||||||
if (currentStep < TOTAL_ONBOARDING_STEPS - 1) {
|
if (currentStep < TOTAL_ONBOARDING_STEPS - 1) {
|
||||||
setCurrentStep(currentStep + 1);
|
setCurrentStep(currentStep + 1);
|
||||||
} else {
|
} else {
|
||||||
// Onboarding is complete - remove from local storage and show layout
|
// Onboarding is complete - remove from local storage and show layout
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
localStorage.removeItem(ONBOARDING_STEP_KEY);
|
localStorage.removeItem(ONBOARDING_STEP_KEY);
|
||||||
}
|
}
|
||||||
setShowLayout(true);
|
setShowLayout(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// List of paths with smaller max-width
|
// List of paths with smaller max-width
|
||||||
const smallWidthPaths = ["/settings/connector/new"];
|
const smallWidthPaths = ["/settings/connector/new"];
|
||||||
const isSmallWidthPath = smallWidthPaths.includes(pathname);
|
const isSmallWidthPath = smallWidthPaths.includes(pathname);
|
||||||
|
|
||||||
const x = showLayout ? "0px" : `calc(-${SIDEBAR_WIDTH / 2}px + 50vw)`;
|
const x = showLayout ? "0px" : `calc(-${SIDEBAR_WIDTH / 2}px + 50vw)`;
|
||||||
const y = showLayout ? "0px" : `calc(-${HEADER_HEIGHT / 2}px + 50vh)`;
|
const y = showLayout ? "0px" : `calc(-${HEADER_HEIGHT / 2}px + 50vh)`;
|
||||||
const translateY = showLayout ? "0px" : `-50vh`;
|
const translateY = showLayout ? "0px" : `-50vh`;
|
||||||
const translateX = showLayout ? "0px" : `-50vw`;
|
const translateX = showLayout ? "0px" : `-50vw`;
|
||||||
|
|
||||||
// For all other pages, render with Langflow-styled navigation and task menu
|
// For all other pages, render with Langflow-styled navigation and task menu
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AnimatedConditional
|
<AnimatedConditional
|
||||||
className="[grid-area:header] bg-background border-b"
|
className="[grid-area:header] bg-background border-b"
|
||||||
vertical
|
vertical
|
||||||
slide
|
slide
|
||||||
isOpen={showLayout}
|
isOpen={showLayout}
|
||||||
delay={ANIMATION_DURATION / 2}
|
delay={ANIMATION_DURATION / 2}
|
||||||
>
|
>
|
||||||
<Header />
|
<Header />
|
||||||
</AnimatedConditional>
|
</AnimatedConditional>
|
||||||
|
|
||||||
{/* Sidebar Navigation */}
|
{/* Sidebar Navigation */}
|
||||||
<AnimatedConditional
|
<AnimatedConditional
|
||||||
isOpen={showLayout}
|
isOpen={showLayout}
|
||||||
slide
|
slide
|
||||||
className={`border-r bg-background overflow-hidden [grid-area:nav] w-[${SIDEBAR_WIDTH}px]`}
|
className={`border-r bg-background overflow-hidden [grid-area:nav] w-[${SIDEBAR_WIDTH}px]`}
|
||||||
>
|
>
|
||||||
<Navigation
|
<Navigation
|
||||||
conversations={conversations}
|
conversations={conversations}
|
||||||
isConversationsLoading={isConversationsLoading}
|
isConversationsLoading={isConversationsLoading}
|
||||||
onNewConversation={handleNewConversation}
|
onNewConversation={handleNewConversation}
|
||||||
/>
|
/>
|
||||||
</AnimatedConditional>
|
</AnimatedConditional>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="overflow-visible w-full flex items-center justify-center [grid-area:main]">
|
<main className="overflow-hidden w-full flex items-center justify-center [grid-area:main]">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{
|
initial={{
|
||||||
width: showLayout ? "100%" : "100vw",
|
width: showLayout ? "100%" : "100vw",
|
||||||
height: showLayout ? "100%" : "100vh",
|
height: showLayout ? "100%" : "100vh",
|
||||||
x: x,
|
x: x,
|
||||||
y: y,
|
y: y,
|
||||||
translateX: translateX,
|
translateX: translateX,
|
||||||
translateY: translateY,
|
translateY: translateY,
|
||||||
}}
|
}}
|
||||||
animate={{
|
animate={{
|
||||||
width: showLayout ? "100%" : "850px",
|
width: showLayout ? "100%" : "850px",
|
||||||
borderRadius: showLayout ? "0" : "16px",
|
borderRadius: showLayout ? "0" : "16px",
|
||||||
border: showLayout ? "0" : "1px solid #27272A",
|
border: showLayout ? "0" : "1px solid #27272A",
|
||||||
height: showLayout ? "100%" : "800px",
|
height: showLayout ? "100%" : "800px",
|
||||||
x: x,
|
x: x,
|
||||||
y: y,
|
y: y,
|
||||||
translateX: translateX,
|
translateX: translateX,
|
||||||
translateY: translateY,
|
translateY: translateY,
|
||||||
}}
|
}}
|
||||||
transition={{
|
transition={{
|
||||||
duration: ANIMATION_DURATION,
|
duration: ANIMATION_DURATION,
|
||||||
ease: "easeOut",
|
ease: "easeOut",
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full w-full max-w-full max-h-full items-center justify-center overflow-hidden",
|
"flex h-full w-full max-w-full max-h-full items-center justify-center overflow-hidden",
|
||||||
!showLayout && "absolute",
|
!showLayout && "absolute",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full bg-background",
|
"h-full bg-background w-full",
|
||||||
showLayout && "p-6 container",
|
showLayout && !isOnChatPage && "p-6 container overflow-y-auto",
|
||||||
showLayout && isSmallWidthPath && "max-w-[850px] ml-0",
|
showLayout && isSmallWidthPath && "max-w-[850px] ml-0",
|
||||||
!showLayout &&
|
!showLayout &&
|
||||||
"w-full bg-card rounded-lg shadow-2xl p-8 overflow-y-auto",
|
"w-full bg-card rounded-lg shadow-2xl p-0 py-2 overflow-y-auto",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{
|
initial={{
|
||||||
opacity: showLayout ? 1 : 0,
|
opacity: showLayout ? 1 : 0,
|
||||||
}}
|
}}
|
||||||
animate={{
|
animate={{
|
||||||
opacity: "100%",
|
opacity: "100%",
|
||||||
}}
|
}}
|
||||||
transition={{
|
transition={{
|
||||||
duration: ANIMATION_DURATION,
|
duration: ANIMATION_DURATION,
|
||||||
ease: "easeOut",
|
ease: "easeOut",
|
||||||
delay: ANIMATION_DURATION,
|
delay: ANIMATION_DURATION,
|
||||||
}}
|
}}
|
||||||
className={cn("w-full h-full 0v")}
|
className={cn("w-full h-full 0v")}
|
||||||
>
|
>
|
||||||
<div className={cn("w-full h-full", !showLayout && "hidden")}>
|
<div className={cn("w-full h-full", !showLayout && "hidden")}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
{!showLayout && (
|
{!showLayout && (
|
||||||
<OnboardingContent
|
<OnboardingContent
|
||||||
handleStepComplete={handleStepComplete}
|
handleStepComplete={handleStepComplete}
|
||||||
currentStep={currentStep}
|
currentStep={currentStep}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: showLayout ? 0 : 1, y: showLayout ? 20 : 0 }}
|
animate={{ opacity: showLayout ? 0 : 1, y: showLayout ? 20 : 0 }}
|
||||||
transition={{ duration: ANIMATION_DURATION, ease: "easeOut" }}
|
transition={{ duration: ANIMATION_DURATION, ease: "easeOut" }}
|
||||||
className={cn("absolute bottom-10 left-0 right-0")}
|
className={cn("absolute bottom-10 left-0 right-0")}
|
||||||
>
|
>
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
currentStep={currentStep}
|
currentStep={currentStep}
|
||||||
totalSteps={TOTAL_ONBOARDING_STEPS}
|
totalSteps={TOTAL_ONBOARDING_STEPS}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
492
frontend/src/hooks/useChatStreaming.ts
Normal file
492
frontend/src/hooks/useChatStreaming.ts
Normal file
|
|
@ -0,0 +1,492 @@
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import type { FunctionCall, Message, SelectedFilters } from "@/app/chat/types";
|
||||||
|
|
||||||
|
interface UseChatStreamingOptions {
|
||||||
|
endpoint?: string;
|
||||||
|
onComplete?: (message: Message, responseId: string | null) => void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SendMessageOptions {
|
||||||
|
prompt: string;
|
||||||
|
previousResponseId?: string;
|
||||||
|
filters?: SelectedFilters;
|
||||||
|
limit?: number;
|
||||||
|
scoreThreshold?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useChatStreaming({
|
||||||
|
endpoint = "/api/langflow",
|
||||||
|
onComplete,
|
||||||
|
onError,
|
||||||
|
}: UseChatStreamingOptions = {}) {
|
||||||
|
const [streamingMessage, setStreamingMessage] = useState<Message | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const streamAbortRef = useRef<AbortController | null>(null);
|
||||||
|
const streamIdRef = useRef(0);
|
||||||
|
|
||||||
|
const sendMessage = async ({
|
||||||
|
prompt,
|
||||||
|
previousResponseId,
|
||||||
|
filters,
|
||||||
|
limit = 10,
|
||||||
|
scoreThreshold = 0,
|
||||||
|
}: SendMessageOptions) => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
// Abort any existing stream before starting a new one
|
||||||
|
if (streamAbortRef.current) {
|
||||||
|
streamAbortRef.current.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
streamAbortRef.current = controller;
|
||||||
|
const thisStreamId = ++streamIdRef.current;
|
||||||
|
|
||||||
|
const requestBody: {
|
||||||
|
prompt: string;
|
||||||
|
stream: boolean;
|
||||||
|
previous_response_id?: string;
|
||||||
|
filters?: SelectedFilters;
|
||||||
|
limit?: number;
|
||||||
|
scoreThreshold?: number;
|
||||||
|
} = {
|
||||||
|
prompt,
|
||||||
|
stream: true,
|
||||||
|
limit,
|
||||||
|
scoreThreshold,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (previousResponseId) {
|
||||||
|
requestBody.previous_response_id = previousResponseId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters) {
|
||||||
|
requestBody.filters = filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestBody),
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
if (!reader) {
|
||||||
|
throw new Error("No reader available");
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = "";
|
||||||
|
let currentContent = "";
|
||||||
|
const currentFunctionCalls: FunctionCall[] = [];
|
||||||
|
let newResponseId: string | null = null;
|
||||||
|
|
||||||
|
// Initialize streaming message
|
||||||
|
if (!controller.signal.aborted && thisStreamId === streamIdRef.current) {
|
||||||
|
setStreamingMessage({
|
||||||
|
role: "assistant",
|
||||||
|
content: "",
|
||||||
|
timestamp: new Date(),
|
||||||
|
isStreaming: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (controller.signal.aborted || thisStreamId !== streamIdRef.current)
|
||||||
|
break;
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
|
// Process complete lines (JSON objects)
|
||||||
|
const lines = buffer.split("\n");
|
||||||
|
buffer = lines.pop() || ""; // Keep incomplete line in buffer
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim()) {
|
||||||
|
try {
|
||||||
|
const chunk = JSON.parse(line);
|
||||||
|
|
||||||
|
// Extract response ID if present
|
||||||
|
if (chunk.id) {
|
||||||
|
newResponseId = chunk.id;
|
||||||
|
} else if (chunk.response_id) {
|
||||||
|
newResponseId = chunk.response_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle OpenAI Chat Completions streaming format
|
||||||
|
if (chunk.object === "response.chunk" && chunk.delta) {
|
||||||
|
// Handle function calls in delta
|
||||||
|
if (chunk.delta.function_call) {
|
||||||
|
if (chunk.delta.function_call.name) {
|
||||||
|
const functionCall: FunctionCall = {
|
||||||
|
name: chunk.delta.function_call.name,
|
||||||
|
arguments: undefined,
|
||||||
|
status: "pending",
|
||||||
|
argumentsString:
|
||||||
|
chunk.delta.function_call.arguments || "",
|
||||||
|
};
|
||||||
|
currentFunctionCalls.push(functionCall);
|
||||||
|
} else if (chunk.delta.function_call.arguments) {
|
||||||
|
const lastFunctionCall =
|
||||||
|
currentFunctionCalls[currentFunctionCalls.length - 1];
|
||||||
|
if (lastFunctionCall) {
|
||||||
|
if (!lastFunctionCall.argumentsString) {
|
||||||
|
lastFunctionCall.argumentsString = "";
|
||||||
|
}
|
||||||
|
lastFunctionCall.argumentsString +=
|
||||||
|
chunk.delta.function_call.arguments;
|
||||||
|
|
||||||
|
if (lastFunctionCall.argumentsString.includes("}")) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(
|
||||||
|
lastFunctionCall.argumentsString
|
||||||
|
);
|
||||||
|
lastFunctionCall.arguments = parsed;
|
||||||
|
lastFunctionCall.status = "completed";
|
||||||
|
} catch (e) {
|
||||||
|
// Arguments not yet complete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle tool calls in delta
|
||||||
|
else if (
|
||||||
|
chunk.delta.tool_calls &&
|
||||||
|
Array.isArray(chunk.delta.tool_calls)
|
||||||
|
) {
|
||||||
|
for (const toolCall of chunk.delta.tool_calls) {
|
||||||
|
if (toolCall.function) {
|
||||||
|
if (toolCall.function.name) {
|
||||||
|
const functionCall: FunctionCall = {
|
||||||
|
name: toolCall.function.name,
|
||||||
|
arguments: undefined,
|
||||||
|
status: "pending",
|
||||||
|
argumentsString: toolCall.function.arguments || "",
|
||||||
|
};
|
||||||
|
currentFunctionCalls.push(functionCall);
|
||||||
|
} else if (toolCall.function.arguments) {
|
||||||
|
const lastFunctionCall =
|
||||||
|
currentFunctionCalls[
|
||||||
|
currentFunctionCalls.length - 1
|
||||||
|
];
|
||||||
|
if (lastFunctionCall) {
|
||||||
|
if (!lastFunctionCall.argumentsString) {
|
||||||
|
lastFunctionCall.argumentsString = "";
|
||||||
|
}
|
||||||
|
lastFunctionCall.argumentsString +=
|
||||||
|
toolCall.function.arguments;
|
||||||
|
|
||||||
|
if (
|
||||||
|
lastFunctionCall.argumentsString.includes("}")
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(
|
||||||
|
lastFunctionCall.argumentsString
|
||||||
|
);
|
||||||
|
lastFunctionCall.arguments = parsed;
|
||||||
|
lastFunctionCall.status = "completed";
|
||||||
|
} catch (e) {
|
||||||
|
// Arguments not yet complete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle content/text in delta
|
||||||
|
else if (chunk.delta.content) {
|
||||||
|
currentContent += chunk.delta.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle finish reason
|
||||||
|
if (chunk.delta.finish_reason) {
|
||||||
|
currentFunctionCalls.forEach((fc) => {
|
||||||
|
if (fc.status === "pending" && fc.argumentsString) {
|
||||||
|
try {
|
||||||
|
fc.arguments = JSON.parse(fc.argumentsString);
|
||||||
|
fc.status = "completed";
|
||||||
|
} catch (e) {
|
||||||
|
fc.arguments = { raw: fc.argumentsString };
|
||||||
|
fc.status = "error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle Realtime API format - function call added
|
||||||
|
else if (
|
||||||
|
chunk.type === "response.output_item.added" &&
|
||||||
|
chunk.item?.type === "function_call"
|
||||||
|
) {
|
||||||
|
let existing = currentFunctionCalls.find(
|
||||||
|
(fc) => fc.id === chunk.item.id
|
||||||
|
);
|
||||||
|
if (!existing) {
|
||||||
|
existing = [...currentFunctionCalls]
|
||||||
|
.reverse()
|
||||||
|
.find(
|
||||||
|
(fc) =>
|
||||||
|
fc.status === "pending" &&
|
||||||
|
!fc.id &&
|
||||||
|
fc.name === (chunk.item.tool_name || chunk.item.name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
existing.id = chunk.item.id;
|
||||||
|
existing.type = chunk.item.type;
|
||||||
|
existing.name =
|
||||||
|
chunk.item.tool_name || chunk.item.name || existing.name;
|
||||||
|
existing.arguments =
|
||||||
|
chunk.item.inputs || existing.arguments;
|
||||||
|
} else {
|
||||||
|
const functionCall: FunctionCall = {
|
||||||
|
name:
|
||||||
|
chunk.item.tool_name || chunk.item.name || "unknown",
|
||||||
|
arguments: chunk.item.inputs || undefined,
|
||||||
|
status: "pending",
|
||||||
|
argumentsString: "",
|
||||||
|
id: chunk.item.id,
|
||||||
|
type: chunk.item.type,
|
||||||
|
};
|
||||||
|
currentFunctionCalls.push(functionCall);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle Realtime API format - tool call added
|
||||||
|
else if (
|
||||||
|
chunk.type === "response.output_item.added" &&
|
||||||
|
chunk.item?.type?.includes("_call") &&
|
||||||
|
chunk.item?.type !== "function_call"
|
||||||
|
) {
|
||||||
|
let existing = currentFunctionCalls.find(
|
||||||
|
(fc) => fc.id === chunk.item.id
|
||||||
|
);
|
||||||
|
if (!existing) {
|
||||||
|
existing = [...currentFunctionCalls]
|
||||||
|
.reverse()
|
||||||
|
.find(
|
||||||
|
(fc) =>
|
||||||
|
fc.status === "pending" &&
|
||||||
|
!fc.id &&
|
||||||
|
fc.name ===
|
||||||
|
(chunk.item.tool_name ||
|
||||||
|
chunk.item.name ||
|
||||||
|
chunk.item.type)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
existing.id = chunk.item.id;
|
||||||
|
existing.type = chunk.item.type;
|
||||||
|
existing.name =
|
||||||
|
chunk.item.tool_name ||
|
||||||
|
chunk.item.name ||
|
||||||
|
chunk.item.type ||
|
||||||
|
existing.name;
|
||||||
|
existing.arguments =
|
||||||
|
chunk.item.inputs || existing.arguments;
|
||||||
|
} else {
|
||||||
|
const functionCall = {
|
||||||
|
name:
|
||||||
|
chunk.item.tool_name ||
|
||||||
|
chunk.item.name ||
|
||||||
|
chunk.item.type ||
|
||||||
|
"unknown",
|
||||||
|
arguments: chunk.item.inputs || {},
|
||||||
|
status: "pending" as const,
|
||||||
|
id: chunk.item.id,
|
||||||
|
type: chunk.item.type,
|
||||||
|
};
|
||||||
|
currentFunctionCalls.push(functionCall);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle function call done
|
||||||
|
else if (
|
||||||
|
chunk.type === "response.output_item.done" &&
|
||||||
|
chunk.item?.type === "function_call"
|
||||||
|
) {
|
||||||
|
const functionCall = currentFunctionCalls.find(
|
||||||
|
(fc) =>
|
||||||
|
fc.id === chunk.item.id ||
|
||||||
|
fc.name === chunk.item.tool_name ||
|
||||||
|
fc.name === chunk.item.name
|
||||||
|
);
|
||||||
|
|
||||||
|
if (functionCall) {
|
||||||
|
functionCall.status =
|
||||||
|
chunk.item.status === "completed" ? "completed" : "error";
|
||||||
|
functionCall.id = chunk.item.id;
|
||||||
|
functionCall.type = chunk.item.type;
|
||||||
|
functionCall.name =
|
||||||
|
chunk.item.tool_name ||
|
||||||
|
chunk.item.name ||
|
||||||
|
functionCall.name;
|
||||||
|
functionCall.arguments =
|
||||||
|
chunk.item.inputs || functionCall.arguments;
|
||||||
|
|
||||||
|
if (chunk.item.results) {
|
||||||
|
functionCall.result = chunk.item.results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle tool call done with results
|
||||||
|
else if (
|
||||||
|
chunk.type === "response.output_item.done" &&
|
||||||
|
chunk.item?.type?.includes("_call") &&
|
||||||
|
chunk.item?.type !== "function_call"
|
||||||
|
) {
|
||||||
|
const functionCall = currentFunctionCalls.find(
|
||||||
|
(fc) =>
|
||||||
|
fc.id === chunk.item.id ||
|
||||||
|
fc.name === chunk.item.tool_name ||
|
||||||
|
fc.name === chunk.item.name ||
|
||||||
|
fc.name === chunk.item.type ||
|
||||||
|
fc.name.includes(chunk.item.type.replace("_call", "")) ||
|
||||||
|
chunk.item.type.includes(fc.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (functionCall) {
|
||||||
|
functionCall.arguments =
|
||||||
|
chunk.item.inputs || functionCall.arguments;
|
||||||
|
functionCall.status =
|
||||||
|
chunk.item.status === "completed" ? "completed" : "error";
|
||||||
|
functionCall.id = chunk.item.id;
|
||||||
|
functionCall.type = chunk.item.type;
|
||||||
|
|
||||||
|
if (chunk.item.results) {
|
||||||
|
functionCall.result = chunk.item.results;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const newFunctionCall = {
|
||||||
|
name:
|
||||||
|
chunk.item.tool_name ||
|
||||||
|
chunk.item.name ||
|
||||||
|
chunk.item.type ||
|
||||||
|
"unknown",
|
||||||
|
arguments: chunk.item.inputs || {},
|
||||||
|
status: "completed" as const,
|
||||||
|
id: chunk.item.id,
|
||||||
|
type: chunk.item.type,
|
||||||
|
result: chunk.item.results,
|
||||||
|
};
|
||||||
|
currentFunctionCalls.push(newFunctionCall);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle text output streaming (Realtime API)
|
||||||
|
else if (chunk.type === "response.output_text.delta") {
|
||||||
|
currentContent += chunk.delta || "";
|
||||||
|
}
|
||||||
|
// Handle OpenRAG backend format
|
||||||
|
else if (chunk.output_text) {
|
||||||
|
currentContent += chunk.output_text;
|
||||||
|
} else if (chunk.delta) {
|
||||||
|
if (typeof chunk.delta === "string") {
|
||||||
|
currentContent += chunk.delta;
|
||||||
|
} else if (typeof chunk.delta === "object") {
|
||||||
|
if (chunk.delta.content) {
|
||||||
|
currentContent += chunk.delta.content;
|
||||||
|
} else if (chunk.delta.text) {
|
||||||
|
currentContent += chunk.delta.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update streaming message in real-time
|
||||||
|
if (
|
||||||
|
!controller.signal.aborted &&
|
||||||
|
thisStreamId === streamIdRef.current
|
||||||
|
) {
|
||||||
|
setStreamingMessage({
|
||||||
|
role: "assistant",
|
||||||
|
content: currentContent,
|
||||||
|
functionCalls:
|
||||||
|
currentFunctionCalls.length > 0
|
||||||
|
? [...currentFunctionCalls]
|
||||||
|
: undefined,
|
||||||
|
timestamp: new Date(),
|
||||||
|
isStreaming: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
console.warn("Failed to parse chunk:", line, parseError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
reader.releaseLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize the message
|
||||||
|
const finalMessage: Message = {
|
||||||
|
role: "assistant",
|
||||||
|
content: currentContent,
|
||||||
|
functionCalls:
|
||||||
|
currentFunctionCalls.length > 0 ? currentFunctionCalls : undefined,
|
||||||
|
timestamp: new Date(),
|
||||||
|
isStreaming: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!controller.signal.aborted && thisStreamId === streamIdRef.current) {
|
||||||
|
// Clear streaming message and call onComplete with final message
|
||||||
|
setStreamingMessage(null);
|
||||||
|
onComplete?.(finalMessage, newResponseId);
|
||||||
|
return finalMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
// If stream was aborted, don't handle as error
|
||||||
|
if (streamAbortRef.current?.signal.aborted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("SSE Stream error:", error);
|
||||||
|
setStreamingMessage(null);
|
||||||
|
onError?.(error as Error);
|
||||||
|
|
||||||
|
const errorMessage: Message = {
|
||||||
|
role: "assistant",
|
||||||
|
content:
|
||||||
|
"Sorry, I couldn't connect to the chat service. Please try again.",
|
||||||
|
timestamp: new Date(),
|
||||||
|
isStreaming: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return errorMessage;
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const abortStream = () => {
|
||||||
|
if (streamAbortRef.current) {
|
||||||
|
streamAbortRef.current.abort();
|
||||||
|
}
|
||||||
|
setStreamingMessage(null);
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
streamingMessage,
|
||||||
|
isLoading,
|
||||||
|
sendMessage,
|
||||||
|
abortStream,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
*/
|
*/
|
||||||
export const DEFAULT_AGENT_SETTINGS = {
|
export const DEFAULT_AGENT_SETTINGS = {
|
||||||
llm_model: "gpt-4o-mini",
|
llm_model: "gpt-4o-mini",
|
||||||
system_prompt: "You are a helpful assistant that can use tools to answer questions and perform tasks."
|
system_prompt: "You are a helpful assistant that can use tools to answer questions and perform tasks. You are part of OpenRAG, an assistant that analyzes documents and provides informations about them. When asked about what is OpenRAG, answer the following:\n\n\"OpenRAG is an open-source package for building agentic RAG systems. It supports integration with a wide range of orchestration tools, vector databases, and LLM providers. OpenRAG connects and amplifies three popular, proven open-source projects into one powerful platform:\n\n**Langflow** – Langflow is a powerful tool to build and deploy AI agents and MCP servers [Read more](https://www.langflow.org/)\n\n**OpenSearch** – Langflow is a powerful tool to build and deploy AI agents and MCP servers [Read more](https://opensearch.org/)\n\n**Docling** – Langflow is a powerful tool to build and deploy AI agents and MCP servers [Read more](https://www.docling.ai/)\""
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ def get_conversation_thread(user_id: str, previous_response_id: str = None):
|
||||||
"messages": [
|
"messages": [
|
||||||
{
|
{
|
||||||
"role": "system",
|
"role": "system",
|
||||||
"content": "You are a helpful assistant. Always use the search_tools to answer questions.",
|
"content": "You are a helpful assistant that can use tools to answer questions and perform tasks. You are part of OpenRAG, an assistant that analyzes documents and provides informations about them. When asked about what is OpenRAG, answer the following:\n\n\"OpenRAG is an open-source package for building agentic RAG systems. It supports integration with a wide range of orchestration tools, vector databases, and LLM providers. OpenRAG connects and amplifies three popular, proven open-source projects into one powerful platform:\n\n**Langflow** – Langflow is a powerful tool to build and deploy AI agents and MCP servers [Read more](https://www.langflow.org/)\n\n**OpenSearch** – Langflow is a powerful tool to build and deploy AI agents and MCP servers [Read more](https://opensearch.org/)\n\n**Docling** – Langflow is a powerful tool to build and deploy AI agents and MCP servers [Read more](https://www.docling.ai/)\"",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"previous_response_id": previous_response_id, # Parent response_id for branching
|
"previous_response_id": previous_response_id, # Parent response_id for branching
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue