Merge branch 'main' into docs-flatten-pdf
This commit is contained in:
commit
26b99a4872
9 changed files with 563 additions and 169 deletions
|
|
@ -3,20 +3,77 @@ interface DogIconProps extends React.SVGProps<SVGSVGElement> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const DogIcon = ({ disabled = false, stroke, ...props }: DogIconProps) => {
|
const DogIcon = ({ disabled = false, stroke, ...props }: DogIconProps) => {
|
||||||
const fillColor = disabled ? "#71717A" : (stroke || "#22A7AF");
|
const fillColor = disabled ? "#71717A" : (stroke || "#773EFF");
|
||||||
|
|
||||||
|
// CSS for the stepped animation states
|
||||||
|
const animationCSS = `
|
||||||
|
.state1 { animation: showState1 600ms infinite; }
|
||||||
|
.state2 { animation: showState2 600ms infinite; }
|
||||||
|
.state3 { animation: showState3 600ms infinite; }
|
||||||
|
.state4 { animation: showState4 600ms infinite; }
|
||||||
|
|
||||||
|
@keyframes showState1 {
|
||||||
|
0%, 24.99% { opacity: 1; }
|
||||||
|
25%, 100% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes showState2 {
|
||||||
|
0%, 24.99% { opacity: 0; }
|
||||||
|
25%, 49.99% { opacity: 1; }
|
||||||
|
50%, 100% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes showState3 {
|
||||||
|
0%, 49.99% { opacity: 0; }
|
||||||
|
50%, 74.99% { opacity: 1; }
|
||||||
|
75%, 100% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes showState4 {
|
||||||
|
0%, 74.99% { opacity: 0; }
|
||||||
|
75%, 100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
disabled ? (
|
disabled ? (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 18" fill={fillColor}>
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 18" fill={fillColor} {...props}>
|
||||||
<path d="M8 18H2V16H8V18Z"/>
|
<path d="M8 18H2V16H8V18Z"/>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 2H22V6H24V10H20V14H24V16H14V14H2V16H0V8H2V6H8V10H10V12H16V6H14V10H12V8H10V2H12V0H20V2ZM18 6H20V4H18V6Z"/>
|
<path fillRule="evenodd" clipRule="evenodd" d="M20 2H22V6H24V10H20V14H24V16H14V14H2V16H0V8H2V6H8V10H10V12H16V6H14V10H12V8H10V2H12V0H20V2ZM18 6H20V4H18V6Z"/>
|
||||||
</svg>
|
</svg>
|
||||||
) : (
|
) : (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="20" viewBox="0 0 24 20" fill={fillColor}>
|
<svg width="105" height="77" viewBox="0 0 105 77" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.0769 10.9091H16.6154V5.45455H14.7692V9.09091H12.9231V7.27273H11.0769V1.81818H12.9231V0H20.3077V1.81818H22.1538V5.45455H24V9.09091H20.3077V14.5455H18.4615V20H14.7692V16.3636H12.9231V14.5455H7.38462V16.3636H5.53846V20H1.84615V10.9091H3.69231V9.09091H11.0769V10.9091ZM18.4615 5.45455H20.3077V3.63636H18.4615V5.45455Z"/>
|
<defs>
|
||||||
<path d="M1.84615 10.9091H0V7.27273H1.84615V10.9091Z"/>
|
<style dangerouslySetInnerHTML={{ __html: animationCSS }} />
|
||||||
<path d="M3.69231 7.27273H1.84615V5.45455H3.69231V7.27273Z"/>
|
</defs>
|
||||||
<path d="M5.53846 5.45455H3.69231V3.63636H5.53846V5.45455Z"/>
|
|
||||||
|
{/* State 1 - Add 14px left padding to align with state 3 */}
|
||||||
|
<g className="state1">
|
||||||
|
<path fillRule="evenodd" clipRule="evenodd" d="M56 42H77V21H70V35H63V28H56V7H63V0H91V7H98V21H105V35H91V56H84V77H70V63H63V56H42V63H35V77H21V42H28V35H56V42ZM84 21H91V14H84V21Z" fill={fillColor}/>
|
||||||
|
<path d="M21 42H14V28H21V42Z" fill={fillColor}/>
|
||||||
|
<path d="M28 28H21V21H28V28Z" fill={fillColor}/>
|
||||||
|
<path d="M35 21H28V14H35V21Z" fill={fillColor}/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* State 2 - Add 14px left padding to align with state 3 */}
|
||||||
|
<g className="state2">
|
||||||
|
<path fillRule="evenodd" clipRule="evenodd" d="M56 42H77V21H70V35H63V28H56V7H63V0H91V7H98V21H105V35H91V56H84V77H70V63H63V56H42V63H35V77H21V42H28V35H56V42ZM84 21H91V14H84V21Z" fill={fillColor}/>
|
||||||
|
<path d="M21 42H14V14H21V42Z" fill={fillColor}/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* State 3 - Already properly positioned */}
|
||||||
|
<g className="state3">
|
||||||
|
<path fillRule="evenodd" clipRule="evenodd" d="M56 42H77V21H70V35H63V28H56V7H63V0H91V7H98V21H105V35H91V56H84V77H70V63H63V56H42V63H35V77H21V42H28V35H56V42ZM84 21H91V14H84V21Z" fill={fillColor}/>
|
||||||
|
<path d="M21 42H14V28H21V42Z" fill={fillColor}/>
|
||||||
|
<path d="M14 28H7V21H14V28Z" fill={fillColor}/>
|
||||||
|
<path d="M7 21H0V14H7V21Z" fill={fillColor}/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* State 4 - Add 14px left padding to align with state 3 */}
|
||||||
|
<g className="state4">
|
||||||
|
<path fillRule="evenodd" clipRule="evenodd" d="M56 42H77V21H70V35H63V28H56V7H63V0H91V7H98V21H105V35H91V56H84V77H70V63H63V56H42V63H35V77H21V42H28V35H56V42ZM84 21H91V14H84V21Z" fill={fillColor}/>
|
||||||
|
<path d="M21 42H14V14H21V42Z" fill={fillColor}/>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ interface AssistantMessageProps {
|
||||||
showForkButton?: boolean;
|
showForkButton?: boolean;
|
||||||
onFork?: (e: React.MouseEvent) => void;
|
onFork?: (e: React.MouseEvent) => void;
|
||||||
isCompleted?: boolean;
|
isCompleted?: boolean;
|
||||||
|
isInactive?: boolean;
|
||||||
animate?: boolean;
|
animate?: boolean;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
}
|
}
|
||||||
|
|
@ -31,6 +32,7 @@ export function AssistantMessage({
|
||||||
showForkButton = false,
|
showForkButton = false,
|
||||||
onFork,
|
onFork,
|
||||||
isCompleted = false,
|
isCompleted = false,
|
||||||
|
isInactive = false,
|
||||||
animate = true,
|
animate = true,
|
||||||
delay = 0.2,
|
delay = 0.2,
|
||||||
}: AssistantMessageProps) {
|
}: AssistantMessageProps) {
|
||||||
|
|
@ -47,7 +49,7 @@ export function AssistantMessage({
|
||||||
<div className="w-8 h-8 rounded-lg bg-accent/20 flex items-center justify-center flex-shrink-0 select-none">
|
<div className="w-8 h-8 rounded-lg bg-accent/20 flex items-center justify-center flex-shrink-0 select-none">
|
||||||
<DogIcon
|
<DogIcon
|
||||||
className="h-6 w-6 transition-colors duration-300"
|
className="h-6 w-6 transition-colors duration-300"
|
||||||
disabled={isCompleted}
|
disabled={isCompleted || isInactive}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { ArrowRight, Check, Funnel, Loader2, Plus, X } from "lucide-react";
|
import { ArrowRight, Check, Funnel, Loader2, Plus } from "lucide-react";
|
||||||
import { forwardRef, useImperativeHandle, useRef } from "react";
|
import { forwardRef, useImperativeHandle, useRef } from "react";
|
||||||
import TextareaAutosize from "react-textarea-autosize";
|
import TextareaAutosize from "react-textarea-autosize";
|
||||||
import type { FilterColor } from "@/components/filter-icon-popover";
|
import type { FilterColor } from "@/components/filter-icon-popover";
|
||||||
import { filterAccentClasses } from "@/components/knowledge-filter-panel";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
|
|
@ -10,6 +9,9 @@ import {
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
import type { KnowledgeFilterData } from "../types";
|
import type { KnowledgeFilterData } from "../types";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { FilePreview } from "./file-preview";
|
||||||
|
import { SelectedKnowledgeFilter } from "./selected-knowledge-filter";
|
||||||
|
|
||||||
export interface ChatInputHandle {
|
export interface ChatInputHandle {
|
||||||
focusInput: () => void;
|
focusInput: () => void;
|
||||||
|
|
@ -26,19 +28,18 @@ interface ChatInputProps {
|
||||||
filterSearchTerm: string;
|
filterSearchTerm: string;
|
||||||
selectedFilterIndex: number;
|
selectedFilterIndex: number;
|
||||||
anchorPosition: { x: number; y: number } | null;
|
anchorPosition: { x: number; y: number } | null;
|
||||||
textareaHeight: number;
|
|
||||||
parsedFilterData: { color?: FilterColor } | null;
|
parsedFilterData: { color?: FilterColor } | null;
|
||||||
|
uploadedFile: File | 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;
|
|
||||||
onFilterSelect: (filter: KnowledgeFilterData | null) => void;
|
onFilterSelect: (filter: KnowledgeFilterData | null) => void;
|
||||||
onAtClick: () => void;
|
onAtClick: () => 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;
|
||||||
|
onFileSelected: (file: File | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
||||||
|
|
@ -53,24 +54,24 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
||||||
filterSearchTerm,
|
filterSearchTerm,
|
||||||
selectedFilterIndex,
|
selectedFilterIndex,
|
||||||
anchorPosition,
|
anchorPosition,
|
||||||
textareaHeight,
|
|
||||||
parsedFilterData,
|
parsedFilterData,
|
||||||
|
uploadedFile,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onChange,
|
onChange,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
onHeightChange,
|
|
||||||
onFilterSelect,
|
onFilterSelect,
|
||||||
onAtClick,
|
onAtClick,
|
||||||
onFilePickerChange,
|
|
||||||
onFilePickerClick,
|
onFilePickerClick,
|
||||||
setSelectedFilter,
|
setSelectedFilter,
|
||||||
setIsFilterHighlighted,
|
setIsFilterHighlighted,
|
||||||
setIsFilterDropdownOpen,
|
setIsFilterDropdownOpen,
|
||||||
|
onFileSelected,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [textareaHeight, setTextareaHeight] = useState(0);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
focusInput: () => {
|
focusInput: () => {
|
||||||
|
|
@ -80,90 +81,143 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const handleFilePickerChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = e.target.files;
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
onFileSelected(files[0]);
|
||||||
|
} else {
|
||||||
|
onFileSelected(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<form onSubmit={onSubmit} className="relative">
|
<form onSubmit={onSubmit} className="relative">
|
||||||
<div className="relative flex items-center w-full p-2 gap-2 rounded-xl border border-input focus-within:ring-1 focus-within:ring-ring">
|
{/* Outer container - flex-col to stack file preview above input */}
|
||||||
{selectedFilter ? (
|
<div className="flex flex-col w-full gap-2 rounded-xl border border-input focus-within:ring-1 focus-within:ring-ring p-2">
|
||||||
<span
|
{/* File Preview Section - Always above */}
|
||||||
className={`inline-flex items-center p-1 rounded-sm text-xs font-medium transition-colors ${
|
{uploadedFile && (
|
||||||
filterAccentClasses[parsedFilterData?.color || "zinc"]
|
<FilePreview
|
||||||
}`}
|
uploadedFile={uploadedFile}
|
||||||
>
|
onClear={() => {
|
||||||
{selectedFilter.name}
|
onFileSelected(null);
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedFilter(null);
|
|
||||||
setIsFilterHighlighted(false);
|
|
||||||
}}
|
|
||||||
className="ml-0.5 rounded-full p-0.5"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="iconSm"
|
|
||||||
className="h-8 w-8 p-0 rounded-md hover:bg-muted/50"
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
}}
|
}}
|
||||||
onClick={onAtClick}
|
|
||||||
data-filter-button
|
|
||||||
>
|
|
||||||
<Funnel className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className="relative flex-1"
|
|
||||||
style={{ height: `${textareaHeight}px` }}
|
|
||||||
>
|
|
||||||
<TextareaAutosize
|
|
||||||
ref={inputRef}
|
|
||||||
value={input}
|
|
||||||
onChange={onChange}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
onHeightChange={onHeightChange}
|
|
||||||
maxRows={7}
|
|
||||||
minRows={1}
|
|
||||||
placeholder="Ask a question..."
|
|
||||||
disabled={loading}
|
|
||||||
className={`w-full text-sm bg-transparent focus-visible:outline-none resize-none`}
|
|
||||||
rows={1}
|
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Main Input Container - flex-row or flex-col based on textarea height */}
|
||||||
|
<div className={`relative flex w-full gap-2 ${
|
||||||
|
textareaHeight > 40 ? 'flex-col' : 'flex-row items-center'
|
||||||
|
}`}>
|
||||||
|
{/* Filter + Textarea Section */}
|
||||||
|
<div className={`flex items-center gap-2 ${textareaHeight > 40 ? 'w-full' : 'flex-1'}`}>
|
||||||
|
{textareaHeight <= 40 && (
|
||||||
|
selectedFilter ? (
|
||||||
|
<SelectedKnowledgeFilter
|
||||||
|
selectedFilter={selectedFilter}
|
||||||
|
parsedFilterData={parsedFilterData}
|
||||||
|
onClear={() => {
|
||||||
|
setSelectedFilter(null);
|
||||||
|
setIsFilterHighlighted(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="iconSm"
|
||||||
|
className="h-8 w-8 p-0 rounded-md hover:bg-muted/50"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
onClick={onAtClick}
|
||||||
|
data-filter-button
|
||||||
|
>
|
||||||
|
<Funnel className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className="relative flex-1"
|
||||||
|
style={{ height: `${textareaHeight}px` }}
|
||||||
|
>
|
||||||
|
<TextareaAutosize
|
||||||
|
ref={inputRef}
|
||||||
|
value={input}
|
||||||
|
onChange={onChange}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onHeightChange={(height) => setTextareaHeight(height)}
|
||||||
|
maxRows={7}
|
||||||
|
minRows={1}
|
||||||
|
placeholder="Ask a question..."
|
||||||
|
disabled={loading}
|
||||||
|
className={`w-full text-sm bg-transparent focus-visible:outline-none resize-none`}
|
||||||
|
rows={1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons Section */}
|
||||||
|
<div className={`flex items-center gap-2 ${textareaHeight > 40 ? 'justify-between w-full' : ''}`}>
|
||||||
|
{textareaHeight > 40 && (
|
||||||
|
selectedFilter ? (
|
||||||
|
<SelectedKnowledgeFilter
|
||||||
|
selectedFilter={selectedFilter}
|
||||||
|
parsedFilterData={parsedFilterData}
|
||||||
|
onClear={() => {
|
||||||
|
setSelectedFilter(null);
|
||||||
|
setIsFilterHighlighted(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="iconSm"
|
||||||
|
className="h-8 w-8 p-0 rounded-md hover:bg-muted/50"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
onClick={onAtClick}
|
||||||
|
data-filter-button
|
||||||
|
>
|
||||||
|
<Funnel className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="iconSm"
|
||||||
|
onClick={onFilePickerClick}
|
||||||
|
disabled={isUploading}
|
||||||
|
className="h-8 w-8 p-0 !rounded-md hover:bg-muted/50"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
type="submit"
|
||||||
|
size="iconSm"
|
||||||
|
disabled={(!input.trim() && !uploadedFile) || loading}
|
||||||
|
className="!rounded-md h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<ArrowRight className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="iconSm"
|
|
||||||
onClick={onFilePickerClick}
|
|
||||||
disabled={isUploading}
|
|
||||||
className="h-8 w-8 p-0 !rounded-md hover:bg-muted/50"
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
type="submit"
|
|
||||||
size="iconSm"
|
|
||||||
disabled={!input.trim() || loading}
|
|
||||||
className="!rounded-md h-8 w-8 p-0"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<ArrowRight className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
onChange={onFilePickerChange}
|
onChange={handleFilePickerChange}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
accept=".pdf,.doc,.docx,.txt,.md,.rtf,.odt"
|
accept=".pdf,.doc,.docx,.txt,.md,.rtf,.odt"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
67
frontend/src/app/chat/components/file-preview.tsx
Normal file
67
frontend/src/app/chat/components/file-preview.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
interface FilePreviewProps {
|
||||||
|
uploadedFile: File;
|
||||||
|
onClear: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatFileSize = (bytes: number): string => {
|
||||||
|
if (bytes === 0) return "0 Bytes";
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ["Bytes", "KB", "MB", "GB"];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFilePreviewUrl = (file: File): string => {
|
||||||
|
if (file.type.startsWith("image/")) {
|
||||||
|
return URL.createObjectURL(file);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FilePreview = ({ uploadedFile, onClear }: FilePreviewProps) => {
|
||||||
|
return (
|
||||||
|
<div className="max-w-[250px] flex items-center gap-2 p-2 bg-muted rounded-lg">
|
||||||
|
{/* File Image Preview */}
|
||||||
|
<div className="flex-shrink-0 w-8 h-8 bg-background rounded border border-input flex items-center justify-center overflow-hidden">
|
||||||
|
{getFilePreviewUrl(uploadedFile) ? (
|
||||||
|
<Image
|
||||||
|
src={getFilePreviewUrl(uploadedFile)}
|
||||||
|
alt="File preview"
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="text-xs font-medium text-muted-foreground">
|
||||||
|
{uploadedFile.name.split(".").pop()?.toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* File Info */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-xs text-muted-foreground font-medium truncate">
|
||||||
|
{uploadedFile.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-xxs text-muted-foreground">
|
||||||
|
{formatFileSize(uploadedFile.size)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Clear Button */}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="iconSm"
|
||||||
|
onClick={onClear}
|
||||||
|
className="flex-shrink-0 h-8 w-8 p-0 rounded-md hover:bg-background/50"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import type { KnowledgeFilterData } from "../types";
|
||||||
|
import { filterAccentClasses } from "@/components/knowledge-filter-panel";
|
||||||
|
import type { FilterColor } from "@/components/filter-icon-popover";
|
||||||
|
|
||||||
|
interface SelectedKnowledgeFilterProps {
|
||||||
|
selectedFilter: KnowledgeFilterData;
|
||||||
|
parsedFilterData: { color?: FilterColor } | null;
|
||||||
|
onClear: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SelectedKnowledgeFilter = ({
|
||||||
|
selectedFilter,
|
||||||
|
parsedFilterData,
|
||||||
|
onClear,
|
||||||
|
}: SelectedKnowledgeFilterProps) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center p-1 rounded-sm text-xs font-medium transition-colors ${
|
||||||
|
filterAccentClasses[parsedFilterData?.color || "zinc"]
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{selectedFilter.name}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClear}
|
||||||
|
className="ml-0.5 rounded-full p-0.5"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -60,7 +60,6 @@ function ChatPage() {
|
||||||
const [availableFilters, setAvailableFilters] = useState<
|
const [availableFilters, setAvailableFilters] = useState<
|
||||||
KnowledgeFilterData[]
|
KnowledgeFilterData[]
|
||||||
>([]);
|
>([]);
|
||||||
const [textareaHeight, setTextareaHeight] = useState(40);
|
|
||||||
const [filterSearchTerm, setFilterSearchTerm] = useState("");
|
const [filterSearchTerm, setFilterSearchTerm] = useState("");
|
||||||
const [selectedFilterIndex, setSelectedFilterIndex] = useState(0);
|
const [selectedFilterIndex, setSelectedFilterIndex] = useState(0);
|
||||||
const [isFilterHighlighted, setIsFilterHighlighted] = useState(false);
|
const [isFilterHighlighted, setIsFilterHighlighted] = useState(false);
|
||||||
|
|
@ -71,6 +70,8 @@ function ChatPage() {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
|
||||||
|
|
||||||
const chatInputRef = useRef<ChatInputHandle>(null);
|
const chatInputRef = useRef<ChatInputHandle>(null);
|
||||||
|
|
||||||
const { scrollToBottom } = useStickToBottomContext();
|
const { scrollToBottom } = useStickToBottomContext();
|
||||||
|
|
@ -127,7 +128,7 @@ function ChatPage() {
|
||||||
|
|
||||||
// Copy all computed styles to the hidden div
|
// Copy all computed styles to the hidden div
|
||||||
for (const style of computedStyle) {
|
for (const style of computedStyle) {
|
||||||
(div.style as any)[style] = computedStyle.getPropertyValue(style);
|
(div.style as unknown as Record<string, string>)[style] = computedStyle.getPropertyValue(style);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the div to be hidden but not un-rendered
|
// Set the div to be hidden but not un-rendered
|
||||||
|
|
@ -247,6 +248,7 @@ function ChatPage() {
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
};
|
};
|
||||||
setMessages((prev) => [...prev.slice(0, -1), pollingMessage]);
|
setMessages((prev) => [...prev.slice(0, -1), pollingMessage]);
|
||||||
|
return null;
|
||||||
} else if (response.ok) {
|
} else if (response.ok) {
|
||||||
// Original flow: Direct response
|
// Original flow: Direct response
|
||||||
|
|
||||||
|
|
@ -282,6 +284,8 @@ function ChatPage() {
|
||||||
// For existing conversations, do a silent refresh to keep backend in sync
|
// For existing conversations, do a silent refresh to keep backend in sync
|
||||||
refreshConversationsSilent();
|
refreshConversationsSilent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result.response_id;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Upload failed: ${response.status}`);
|
throw new Error(`Upload failed: ${response.status}`);
|
||||||
|
|
@ -304,13 +308,6 @@ function ChatPage() {
|
||||||
chatInputRef.current?.clickFileInput();
|
chatInputRef.current?.clickFileInput();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilePickerChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const files = e.target.files;
|
|
||||||
if (files && files.length > 0) {
|
|
||||||
handleFileUpload(files[0]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadAvailableFilters = async () => {
|
const loadAvailableFilters = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/knowledge-filter/search", {
|
const response = await fetch("/api/knowledge-filter/search", {
|
||||||
|
|
@ -601,6 +598,7 @@ function ChatPage() {
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
|
setUploadedFile(null); // Clear previous file
|
||||||
|
|
||||||
// Add initial upload message
|
// Add initial upload message
|
||||||
const uploadStartMessage: Message = {
|
const uploadStartMessage: Message = {
|
||||||
|
|
@ -627,6 +625,7 @@ function ChatPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
setMessages((prev) => [...prev.slice(0, -1), uploadMessage]);
|
setMessages((prev) => [...prev.slice(0, -1), uploadMessage]);
|
||||||
|
setUploadedFile(null); // Clear file after upload
|
||||||
|
|
||||||
// Update the response ID for this endpoint
|
// Update the response ID for this endpoint
|
||||||
if (result.response_id) {
|
if (result.response_id) {
|
||||||
|
|
@ -658,6 +657,7 @@ function ChatPage() {
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
};
|
};
|
||||||
setMessages((prev) => [...prev.slice(0, -1), errorMessage]);
|
setMessages((prev) => [...prev.slice(0, -1), errorMessage]);
|
||||||
|
setUploadedFile(null); // Clear file on error
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener(
|
window.addEventListener(
|
||||||
|
|
@ -724,7 +724,7 @@ function ChatPage() {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSSEStream = async (userMessage: Message) => {
|
const handleSSEStream = async (userMessage: Message, previousResponseId?: string) => {
|
||||||
// Prepare filters
|
// Prepare filters
|
||||||
const processedFilters = parsedFilterData?.filters
|
const processedFilters = parsedFilterData?.filters
|
||||||
? (() => {
|
? (() => {
|
||||||
|
|
@ -750,10 +750,13 @@ function ChatPage() {
|
||||||
})()
|
})()
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
// Use passed previousResponseId if available, otherwise fall back to state
|
||||||
|
const responseIdToUse = previousResponseId || previousResponseIds[endpoint];
|
||||||
|
|
||||||
// Use the hook to send the message
|
// Use the hook to send the message
|
||||||
await sendStreamingMessage({
|
await sendStreamingMessage({
|
||||||
prompt: userMessage.content,
|
prompt: userMessage.content,
|
||||||
previousResponseId: previousResponseIds[endpoint] || undefined,
|
previousResponseId: responseIdToUse || undefined,
|
||||||
filters: processedFilters,
|
filters: processedFilters,
|
||||||
limit: parsedFilterData?.limit ?? 10,
|
limit: parsedFilterData?.limit ?? 10,
|
||||||
scoreThreshold: parsedFilterData?.scoreThreshold ?? 0,
|
scoreThreshold: parsedFilterData?.scoreThreshold ?? 0,
|
||||||
|
|
@ -764,7 +767,7 @@ function ChatPage() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSendMessage = async (inputMessage: string) => {
|
const handleSendMessage = async (inputMessage: string, previousResponseId?: string) => {
|
||||||
if (!inputMessage.trim() || loading) return;
|
if (!inputMessage.trim() || loading) return;
|
||||||
|
|
||||||
const userMessage: Message = {
|
const userMessage: Message = {
|
||||||
|
|
@ -784,7 +787,7 @@ function ChatPage() {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (asyncMode) {
|
if (asyncMode) {
|
||||||
await handleSSEStream(userMessage);
|
await handleSSEStream(userMessage, previousResponseId);
|
||||||
} else {
|
} else {
|
||||||
// Original non-streaming logic
|
// Original non-streaming logic
|
||||||
try {
|
try {
|
||||||
|
|
@ -891,7 +894,30 @@ function ChatPage() {
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSendMessage(input);
|
|
||||||
|
// Check if there's an uploaded file and upload it first
|
||||||
|
let uploadedResponseId: string | null = null;
|
||||||
|
if (uploadedFile) {
|
||||||
|
// Upload the file first
|
||||||
|
const responseId = await handleFileUpload(uploadedFile);
|
||||||
|
// Clear the file after upload
|
||||||
|
setUploadedFile(null);
|
||||||
|
|
||||||
|
// If the upload resulted in a new conversation, store the response ID
|
||||||
|
if (responseId) {
|
||||||
|
uploadedResponseId = responseId;
|
||||||
|
setPreviousResponseIds((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[endpoint]: responseId,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only send message if there's input text
|
||||||
|
if (input.trim()) {
|
||||||
|
// Pass the responseId from upload (if any) to handleSendMessage
|
||||||
|
handleSendMessage(input, uploadedResponseId || undefined);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleFunctionCall = (functionCallId: string) => {
|
const toggleFunctionCall = (functionCallId: string) => {
|
||||||
|
|
@ -1209,7 +1235,7 @@ function ChatPage() {
|
||||||
className="space-y-6 group"
|
className="space-y-6 group"
|
||||||
>
|
>
|
||||||
<UserMessage
|
<UserMessage
|
||||||
animate={message.source !== "langflow"}
|
animate={message.source ? message.source !== "langflow" : false}
|
||||||
content={message.content}
|
content={message.content}
|
||||||
files={
|
files={
|
||||||
index >= 2
|
index >= 2
|
||||||
|
|
@ -1241,6 +1267,7 @@ function ChatPage() {
|
||||||
showForkButton={endpoint === "chat"}
|
showForkButton={endpoint === "chat"}
|
||||||
onFork={(e) => handleForkConversation(index, e)}
|
onFork={(e) => handleForkConversation(index, e)}
|
||||||
animate={false}
|
animate={false}
|
||||||
|
isInactive={index < messages.length - 1}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1257,6 +1284,7 @@ function ChatPage() {
|
||||||
onToggle={toggleFunctionCall}
|
onToggle={toggleFunctionCall}
|
||||||
delay={0.4}
|
delay={0.4}
|
||||||
isStreaming
|
isStreaming
|
||||||
|
isCompleted={false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
@ -1284,16 +1312,15 @@ function ChatPage() {
|
||||||
filterSearchTerm={filterSearchTerm}
|
filterSearchTerm={filterSearchTerm}
|
||||||
selectedFilterIndex={selectedFilterIndex}
|
selectedFilterIndex={selectedFilterIndex}
|
||||||
anchorPosition={anchorPosition}
|
anchorPosition={anchorPosition}
|
||||||
textareaHeight={textareaHeight}
|
|
||||||
parsedFilterData={parsedFilterData}
|
parsedFilterData={parsedFilterData}
|
||||||
|
uploadedFile={uploadedFile}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onHeightChange={(height) => setTextareaHeight(height)}
|
|
||||||
onFilterSelect={handleFilterSelect}
|
onFilterSelect={handleFilterSelect}
|
||||||
onAtClick={onAtClick}
|
onAtClick={onAtClick}
|
||||||
onFilePickerChange={handleFilePickerChange}
|
|
||||||
onFilePickerClick={handleFilePickerClick}
|
onFilePickerClick={handleFilePickerClick}
|
||||||
|
onFileSelected={setUploadedFile}
|
||||||
setSelectedFilter={setSelectedFilter}
|
setSelectedFilter={setSelectedFilter}
|
||||||
setIsFilterHighlighted={setIsFilterHighlighted}
|
setIsFilterHighlighted={setIsFilterHighlighted}
|
||||||
setIsFilterDropdownOpen={setIsFilterDropdownOpen}
|
setIsFilterDropdownOpen={setIsFilterDropdownOpen}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export function ProgressBar({ currentStep, totalSteps }: ProgressBarProps) {
|
||||||
className="h-full transition-all duration-300 ease-in-out"
|
className="h-full transition-all duration-300 ease-in-out"
|
||||||
style={{
|
style={{
|
||||||
width: `${progressPercentage}%`,
|
width: `${progressPercentage}%`,
|
||||||
background: 'linear-gradient(to right, #818CF8, #22A7AF)'
|
background: 'linear-gradient(to right, #773EFF, #22A7AF)'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,70 +1,224 @@
|
||||||
import { cn } from "@/lib/utils";
|
const AnimatedProcessingIcon = ({
|
||||||
import { motion, easeInOut } from "framer-motion";
|
|
||||||
|
|
||||||
export const AnimatedProcessingIcon = ({
|
|
||||||
className,
|
className,
|
||||||
|
props,
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
props?: React.SVGProps<SVGSVGElement>;
|
||||||
}) => {
|
}) => {
|
||||||
const createAnimationFrames = (delay: number) => ({
|
// CSS for the stepped animation states
|
||||||
opacity: [1, 1, 0.5, 0], // Opacity Steps
|
const animationCSS = `
|
||||||
transition: {
|
.state-1 { opacity: 1; animation: showState1 1.5s infinite steps(1, end); }
|
||||||
delay,
|
.state-2 { opacity: 0; animation: showState2 1.5s infinite steps(1, end); }
|
||||||
duration: 1,
|
.state-3 { opacity: 0; animation: showState3 1.5s infinite steps(1, end); }
|
||||||
ease: easeInOut,
|
.state-4 { opacity: 0; animation: showState4 1.5s infinite steps(1, end); }
|
||||||
repeat: Infinity,
|
.state-5 { opacity: 0; animation: showState5 1.5s infinite steps(1, end); }
|
||||||
times: [0, 0.33, 0.66, 1], // Duration Percentages that Correspond to opacity Array
|
.state-6 { opacity: 0; animation: showState6 1.5s infinite steps(1, end); }
|
||||||
},
|
|
||||||
});
|
@keyframes showState1 {
|
||||||
|
0%, 16.66% { opacity: 1; }
|
||||||
|
16.67%, 100% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes showState2 {
|
||||||
|
0%, 16.66% { opacity: 0; }
|
||||||
|
16.67%, 33.33% { opacity: 1; }
|
||||||
|
33.34%, 100% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes showState3 {
|
||||||
|
0%, 33.33% { opacity: 0; }
|
||||||
|
33.34%, 50% { opacity: 1; }
|
||||||
|
50.01%, 100% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes showState4 {
|
||||||
|
0%, 50% { opacity: 0; }
|
||||||
|
50.01%, 66.66% { opacity: 1; }
|
||||||
|
66.67%, 100% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes showState5 {
|
||||||
|
0%, 66.66% { opacity: 0; }
|
||||||
|
66.67%, 83.33% { opacity: 1; }
|
||||||
|
83.34%, 100% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes showState6 {
|
||||||
|
0%, 83.33% { opacity: 0; }
|
||||||
|
83.34%, 100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
data-testid="rotating-dot-animation"
|
width="16"
|
||||||
className={cn("h-[10px] w-[6px]", className)}
|
height="16"
|
||||||
viewBox="0 0 6 10"
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className={className}
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
<motion.circle
|
{/* Inject animation styles into the SVG's shadow */}
|
||||||
animate={createAnimationFrames(0)}
|
<style dangerouslySetInnerHTML={{ __html: animationCSS }} />
|
||||||
fill="currentColor"
|
|
||||||
cx="1"
|
{/* State 1 */}
|
||||||
cy="1"
|
<g className="state-1">
|
||||||
r="1"
|
<rect
|
||||||
/>
|
x="-19.5"
|
||||||
<motion.circle
|
y="-19.5"
|
||||||
animate={createAnimationFrames(0.16)}
|
width="230.242"
|
||||||
fill="currentColor"
|
height="63"
|
||||||
cx="1"
|
rx="4.5"
|
||||||
cy="5"
|
stroke="#9747FF"
|
||||||
r="1"
|
strokeDasharray="10 5"
|
||||||
/>
|
/>
|
||||||
<motion.circle
|
</g>
|
||||||
animate={createAnimationFrames(0.33)}
|
|
||||||
fill="currentColor"
|
{/* State 2 */}
|
||||||
cx="1"
|
<g className="state-2">
|
||||||
cy="9"
|
<rect
|
||||||
r="1"
|
x="-53.5"
|
||||||
/>
|
y="-19.5"
|
||||||
<motion.circle
|
width="230.242"
|
||||||
animate={createAnimationFrames(0.83)}
|
height="63"
|
||||||
fill="currentColor"
|
rx="4.5"
|
||||||
cx="5"
|
stroke="#9747FF"
|
||||||
cy="1"
|
strokeDasharray="10 5"
|
||||||
r="1"
|
/>
|
||||||
/>
|
<path
|
||||||
<motion.circle
|
d="M7.625 20.375H8V20.75H8.375V22.25H8V23H5V22.25H4.625V20.75H5V20.375H5.375V19.625H7.625V20.375Z"
|
||||||
animate={createAnimationFrames(0.66)}
|
fill="currentColor"
|
||||||
fill="currentColor"
|
/>
|
||||||
cx="5"
|
<path d="M4.625 20H3.125V18.5H4.625V20Z" fill="currentColor" />
|
||||||
cy="5"
|
<path d="M9.875 20H8.375V18.5H9.875V20Z" fill="currentColor" />
|
||||||
r="1"
|
<path d="M6.125 18.5H4.625V17H6.125V18.5Z" fill="currentColor" />
|
||||||
/>
|
<path d="M8.375 18.5H6.875V17H8.375V18.5Z" fill="currentColor" />
|
||||||
<motion.circle
|
</g>
|
||||||
animate={createAnimationFrames(0.5)}
|
|
||||||
fill="currentColor"
|
{/* State 3 */}
|
||||||
cx="5"
|
<g className="state-3">
|
||||||
cy="9"
|
<rect
|
||||||
r="1"
|
x="-87.5"
|
||||||
/>
|
y="-19.5"
|
||||||
|
width="230.242"
|
||||||
|
height="63"
|
||||||
|
rx="4.5"
|
||||||
|
stroke="#9747FF"
|
||||||
|
strokeDasharray="10 5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M7.625 20.375H8V20.75H8.375V22.25H8V23H5V22.25H4.625V20.75H5V20.375H5.375V19.625H7.625V20.375Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path d="M4.625 20H3.125V18.5H4.625V20Z" fill="currentColor" />
|
||||||
|
<path d="M9.875 20H8.375V18.5H9.875V20Z" fill="currentColor" />
|
||||||
|
<path d="M6.125 18.5H4.625V17H6.125V18.5Z" fill="currentColor" />
|
||||||
|
<path d="M8.375 18.5H6.875V17H8.375V18.5Z" fill="currentColor" />
|
||||||
|
<path
|
||||||
|
d="M18.625 12.375H19V12.75H19.375V14.25H19V15H16V14.25H15.625V12.75H16V12.375H16.375V11.625H18.625V12.375Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path d="M15.625 12H14.125V10.5H15.625V12Z" fill="currentColor" />
|
||||||
|
<path d="M20.875 12H19.375V10.5H20.875V12Z" fill="currentColor" />
|
||||||
|
<path d="M17.125 10.5H15.625V9H17.125V10.5Z" fill="currentColor" />
|
||||||
|
<path d="M19.375 10.5H17.875V9H19.375V10.5Z" fill="currentColor" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* State 4 */}
|
||||||
|
<g className="state-4">
|
||||||
|
<rect
|
||||||
|
x="-122.5"
|
||||||
|
y="-19.5"
|
||||||
|
width="230.242"
|
||||||
|
height="63"
|
||||||
|
rx="4.5"
|
||||||
|
stroke="#9747FF"
|
||||||
|
strokeDasharray="10 5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M7.625 4.375H8V4.75H8.375V6.25H8V7H5V6.25H4.625V4.75H5V4.375H5.375V3.625H7.625V4.375Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path d="M4.625 4H3.125V2.5H4.625V4Z" fill="currentColor" />
|
||||||
|
<path d="M9.875 4H8.375V2.5H9.875V4Z" fill="currentColor" />
|
||||||
|
<path d="M6.125 2.5H4.625V1H6.125V2.5Z" fill="currentColor" />
|
||||||
|
<path d="M8.375 2.5H6.875V1H8.375V2.5Z" fill="currentColor" />
|
||||||
|
<g opacity="0.25">
|
||||||
|
<path
|
||||||
|
d="M7.625 20.375H8V20.75H8.375V22.25H8V23H5V22.25H4.625V20.75H5V20.375H5.375V19.625H7.625V20.375Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path d="M4.625 20H3.125V18.5H4.625V20Z" fill="currentColor" />
|
||||||
|
<path d="M9.875 20H8.375V18.5H9.875V20Z" fill="currentColor" />
|
||||||
|
<path d="M6.125 18.5H4.625V17H6.125V18.5Z" fill="currentColor" />
|
||||||
|
<path d="M8.375 18.5H6.875V17H8.375V18.5Z" fill="currentColor" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
d="M18.625 12.375H19V12.75H19.375V14.25H19V15H16V14.25H15.625V12.75H16V12.375H16.375V11.625H18.625V12.375Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path d="M15.625 12H14.125V10.5H15.625V12Z" fill="currentColor" />
|
||||||
|
<path d="M20.875 12H19.375V10.5H20.875V12Z" fill="currentColor" />
|
||||||
|
<path d="M17.125 10.5H15.625V9H17.125V10.5Z" fill="currentColor" />
|
||||||
|
<path d="M19.375 10.5H17.875V9H19.375V10.5Z" fill="currentColor" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* State 5 */}
|
||||||
|
<g className="state-5">
|
||||||
|
<rect
|
||||||
|
x="-156.5"
|
||||||
|
y="-19.5"
|
||||||
|
width="230.242"
|
||||||
|
height="63"
|
||||||
|
rx="4.5"
|
||||||
|
stroke="#9747FF"
|
||||||
|
strokeDasharray="10 5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M7.625 4.375H8V4.75H8.375V6.25H8V7H5V6.25H4.625V4.75H5V4.375H5.375V3.625H7.625V4.375Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path d="M4.625 4H3.125V2.5H4.625V4Z" fill="currentColor" />
|
||||||
|
<path d="M9.875 4H8.375V2.5H9.875V4Z" fill="currentColor" />
|
||||||
|
<path d="M6.125 2.5H4.625V1H6.125V2.5Z" fill="currentColor" />
|
||||||
|
<path d="M8.375 2.5H6.875V1H8.375V2.5Z" fill="currentColor" />
|
||||||
|
<g opacity="0.25">
|
||||||
|
<path
|
||||||
|
d="M18.625 12.375H19V12.75H19.375V14.25H19V15H16V14.25H15.625V12.75H16V12.375H16.375V11.625H18.625V12.375Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path d="M15.625 12H14.125V10.5H15.625V12Z" fill="currentColor" />
|
||||||
|
<path d="M20.875 12H19.375V10.5H20.875V12Z" fill="currentColor" />
|
||||||
|
<path d="M17.125 10.5H15.625V9H17.125V10.5Z" fill="currentColor" />
|
||||||
|
<path d="M19.375 10.5H17.875V9H19.375V10.5Z" fill="currentColor" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* State 6 */}
|
||||||
|
<g className="state-6">
|
||||||
|
<rect
|
||||||
|
x="-190.5"
|
||||||
|
y="-19.5"
|
||||||
|
width="230.242"
|
||||||
|
height="63"
|
||||||
|
rx="4.5"
|
||||||
|
stroke="#9747FF"
|
||||||
|
strokeDasharray="10 5"
|
||||||
|
/>
|
||||||
|
<g opacity="0.25">
|
||||||
|
<path
|
||||||
|
d="M7.625 4.375H8V4.75H8.375V6.25H8V7H5V6.25H4.625V4.75H5V4.375H5.375V3.625H7.625V4.375Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path d="M4.625 4H3.125V2.5H4.625V4Z" fill="currentColor" />
|
||||||
|
<path d="M9.875 4H8.375V2.5H9.875V4Z" fill="currentColor" />
|
||||||
|
<path d="M6.125 2.5H4.625V1H6.125V2.5Z" fill="currentColor" />
|
||||||
|
<path d="M8.375 2.5H6.875V1H8.375V2.5Z" fill="currentColor" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default AnimatedProcessingIcon;
|
||||||
|
|
@ -130,7 +130,7 @@ export function KnowledgeFilterProvider({
|
||||||
},
|
},
|
||||||
limit: 10,
|
limit: 10,
|
||||||
scoreThreshold: 0,
|
scoreThreshold: 0,
|
||||||
color: "zinc",
|
color: "amber",
|
||||||
icon: "filter",
|
icon: "filter",
|
||||||
});
|
});
|
||||||
setIsPanelOpen(true);
|
setIsPanelOpen(true);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue