mirror of
https://github.com/harivansh-afk/RAG-ui.git
synced 2026-04-15 07:04:48 +00:00
Made chat history get data from server side instead of client side local
This commit is contained in:
parent
30128368d9
commit
94a353993c
9 changed files with 511 additions and 268 deletions
39
package-lock.json
generated
39
package-lock.json
generated
|
|
@ -8,6 +8,7 @@
|
||||||
"name": "vite-react-typescript-starter",
|
"name": "vite-react-typescript-starter",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||||
"@radix-ui/react-avatar": "^1.0.4",
|
"@radix-ui/react-avatar": "^1.0.4",
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
|
|
@ -24,6 +25,7 @@
|
||||||
"@react-three/drei": "^9.120.3",
|
"@react-three/drei": "^9.120.3",
|
||||||
"@react-three/fiber": "^8.17.10",
|
"@react-three/fiber": "^8.17.10",
|
||||||
"@supabase/supabase-js": "^2.47.0",
|
"@supabase/supabase-js": "^2.47.0",
|
||||||
|
"@tailwindcss/line-clamp": "^0.4.4",
|
||||||
"@types/three": "^0.170.0",
|
"@types/three": "^0.170.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
|
|
@ -1088,6 +1090,34 @@
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
|
||||||
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="
|
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-alert-dialog": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-eGSlLzPhKO+TErxkiGcCZGuvbVMnLA1MTnyBksGOeGRGkxHiiJUujsjmNTdWTm4iHVSRaUao9/4Ur671auMghQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.0",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-dialog": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-slot": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-arrow": {
|
"node_modules/@radix-ui/react-arrow": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
|
||||||
|
|
@ -2483,6 +2513,15 @@
|
||||||
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20"
|
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/line-clamp": {
|
||||||
|
"version": "0.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.4.tgz",
|
||||||
|
"integrity": "sha512-5U6SY5z8N42VtrCrKlsTAA35gy2VSyYtHWCsg1H87NU1SXnEfekTVlrga9fzUDrrHcGi2Lb5KenUWb4lRQT5/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tweenjs/tween.js": {
|
"node_modules/@tweenjs/tween.js": {
|
||||||
"version": "23.1.3",
|
"version": "23.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||||
"@radix-ui/react-avatar": "^1.0.4",
|
"@radix-ui/react-avatar": "^1.0.4",
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
|
|
@ -26,6 +27,7 @@
|
||||||
"@react-three/drei": "^9.120.3",
|
"@react-three/drei": "^9.120.3",
|
||||||
"@react-three/fiber": "^8.17.10",
|
"@react-three/fiber": "^8.17.10",
|
||||||
"@supabase/supabase-js": "^2.47.0",
|
"@supabase/supabase-js": "^2.47.0",
|
||||||
|
"@tailwindcss/line-clamp": "^0.4.4",
|
||||||
"@types/three": "^0.170.0",
|
"@types/three": "^0.170.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
|
|
|
||||||
|
|
@ -1,69 +1,47 @@
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
||||||
import { vs } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
|
||||||
import { Copy, Check, BookOpen, Link as LinkIcon, ThumbsUp, ThumbsDown } from 'lucide-react';
|
|
||||||
import { cn } from '../../lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
import { Button } from '../ui/Button';
|
import { Button } from '../ui/Button';
|
||||||
import { Avatar, AvatarFallback } from '../ui/Avatar';
|
|
||||||
import { Tooltip } from '../ui/Tooltip';
|
import { Tooltip } from '../ui/Tooltip';
|
||||||
|
import { ThumbsUp, ThumbsDown, Copy, Check, User, Bot, Wrench } from 'lucide-react';
|
||||||
|
import { Avatar, AvatarFallback } from '../ui/Avatar';
|
||||||
import { Separator } from '../ui/separator';
|
import { Separator } from '../ui/separator';
|
||||||
import type { ChatMessage } from '../../types/supabase';
|
import type { ChatMessage } from '../../types/supabase';
|
||||||
import type { Components } from 'react-markdown';
|
|
||||||
|
|
||||||
interface MessageProps {
|
interface MessageProps {
|
||||||
message: ChatMessage;
|
message: ChatMessage;
|
||||||
|
isLast?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CodeBlockProps {
|
function extractUsedTools(content: string): { cleanContent: string; usedTools: string[] } {
|
||||||
language: string;
|
const toolRegex = /^\*\*Used Tools?:([^*]+)\*\*\n\n/;
|
||||||
value: string;
|
const match = content.match(toolRegex);
|
||||||
}
|
|
||||||
|
|
||||||
interface Citation {
|
|
||||||
text: string;
|
|
||||||
source: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractCitations(content: string): { cleanContent: string; citations: Citation[] } {
|
|
||||||
const citationRegex = /\[([^\]]+)\]\(((?:(?!\)\[).)+)\)/g;
|
|
||||||
const citations: Citation[] = [];
|
|
||||||
const cleanContent = content.replace(citationRegex, (_, text, source) => {
|
|
||||||
citations.push({ text, source });
|
|
||||||
return text;
|
|
||||||
});
|
|
||||||
return { cleanContent, citations };
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractUsedTool(content: string): { cleanContent: string; usedTools: string[] } {
|
|
||||||
const usedToolRegex = /^\*\*Used Tool(?:s)?: ([^*]+)\*\*\n\n/;
|
|
||||||
const match = content.match(usedToolRegex);
|
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
const usedToolsString = match[1];
|
const toolsString = match[1].trim();
|
||||||
const usedTools = usedToolsString.split(',').map(tool => tool.trim());
|
// Simply split by newlines and clean up each line
|
||||||
const cleanContent = content.replace(usedToolRegex, '');
|
const usedTools = toolsString
|
||||||
|
.split('\n')
|
||||||
|
.map(tool => tool.trim())
|
||||||
|
.filter(tool => tool.length > 0);
|
||||||
|
|
||||||
|
const cleanContent = content.replace(toolRegex, '');
|
||||||
return { cleanContent, usedTools };
|
return { cleanContent, usedTools };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { cleanContent: content, usedTools: [] };
|
return { cleanContent: content, usedTools: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Message({ message }: MessageProps) {
|
export const Message: React.FC<MessageProps> = ({ message, isLast }) => {
|
||||||
const [copied, setCopied] = React.useState(false);
|
const isUser = message.role === 'user';
|
||||||
const [messageCopied, setMessageCopied] = React.useState(false);
|
const [messageCopied, setMessageCopied] = React.useState(false);
|
||||||
const [isLiked, setIsLiked] = React.useState(false);
|
const [isLiked, setIsLiked] = React.useState(false);
|
||||||
const [isDisliked, setIsDisliked] = React.useState(false);
|
const [isDisliked, setIsDisliked] = React.useState(false);
|
||||||
const { cleanContent, citations } = extractCitations(message.content);
|
|
||||||
const { cleanContent: finalContent, usedTools } = extractUsedTool(cleanContent);
|
|
||||||
|
|
||||||
const handleCopy = async (text: string) => {
|
const { cleanContent, usedTools } = useMemo(() =>
|
||||||
await navigator.clipboard.writeText(text);
|
extractUsedTools(message.content), [message.content]
|
||||||
setCopied(true);
|
);
|
||||||
setTimeout(() => setCopied(false), 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMessageCopy = async () => {
|
const handleMessageCopy = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -83,150 +61,74 @@ export function Message({ message }: MessageProps) {
|
||||||
setIsDisliked(!isDisliked);
|
setIsDisliked(!isDisliked);
|
||||||
setIsLiked(false);
|
setIsLiked(false);
|
||||||
}
|
}
|
||||||
// Here you could send feedback to your backend
|
|
||||||
};
|
|
||||||
|
|
||||||
const CodeBlock = React.memo(({ language, value }: CodeBlockProps) => (
|
|
||||||
<div className="relative my-4 rounded-lg overflow-hidden bg-muted/30 transition-all duration-200 hover:bg-muted/40 group">
|
|
||||||
<div className="absolute right-2 top-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
||||||
<Tooltip content={copied ? 'Copied!' : 'Copy code'}>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-8 w-8 p-0 bg-background/80 backdrop-blur-sm hover:bg-background/90"
|
|
||||||
onClick={() => handleCopy(value)}
|
|
||||||
>
|
|
||||||
{copied ? (
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<Copy className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<div className="bg-muted/50 backdrop-blur-sm px-4 py-2 text-xs font-medium text-muted-foreground/80 flex items-center justify-between">
|
|
||||||
<span>{language}</span>
|
|
||||||
<span className="text-xs text-muted-foreground/60">{value.split('\n').length} lines</span>
|
|
||||||
</div>
|
|
||||||
<SyntaxHighlighter
|
|
||||||
language={language}
|
|
||||||
style={vs}
|
|
||||||
customStyle={{ margin: 0, background: 'transparent', padding: '1rem' }}
|
|
||||||
PreTag="div"
|
|
||||||
className="!bg-transparent"
|
|
||||||
>
|
|
||||||
{value.replace(/\n$/, '')}
|
|
||||||
</SyntaxHighlighter>
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
|
|
||||||
const code: Components['code'] = ({ className, children, ...props }) => {
|
|
||||||
const match = /language-(\w+)/.exec(className || '');
|
|
||||||
const language = match ? match[1] : '';
|
|
||||||
const value = String(children).replace(/\n$/, '');
|
|
||||||
|
|
||||||
if (!className) {
|
|
||||||
return (
|
|
||||||
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-sm" {...props}>
|
|
||||||
{children}
|
|
||||||
</code>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <CodeBlock language={language} value={value} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
const components: Components = {
|
|
||||||
code,
|
|
||||||
h1: ({ children }) => (
|
|
||||||
<h1 className="text-xl font-semibold tracking-tight mt-6 mb-4">
|
|
||||||
{children}
|
|
||||||
</h1>
|
|
||||||
),
|
|
||||||
h2: ({ children }) => (
|
|
||||||
<h2 className="text-lg font-semibold tracking-tight mt-5 mb-3">
|
|
||||||
{children}
|
|
||||||
</h2>
|
|
||||||
),
|
|
||||||
h3: ({ children }) => (
|
|
||||||
<h3 className="text-base font-semibold tracking-tight mt-4 mb-2">
|
|
||||||
{children}
|
|
||||||
</h3>
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'group flex w-full gap-3 px-4 animate-in fade-in slide-in-from-bottom-4 duration-300',
|
'group flex w-full items-start gap-4 px-4',
|
||||||
message.role === 'user' ? 'justify-end' : 'justify-start'
|
isUser && 'flex-row-reverse'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{message.role === 'assistant' && (
|
<Avatar className={cn(
|
||||||
<Avatar className="h-8 w-8 ring-2 ring-purple-500/20 transition-all duration-300 group-hover:ring-purple-500/40">
|
'flex h-8 w-8 shrink-0 select-none overflow-hidden rounded-full',
|
||||||
<AvatarFallback className="bg-gradient-to-br from-purple-300 to-purple-500 text-white text-xs font-medium">
|
isUser
|
||||||
AI
|
? 'ring-2 ring-secondary/20 hover:ring-secondary/40 transition-all duration-300'
|
||||||
</AvatarFallback>
|
: 'ring-2 ring-purple-500/20 hover:ring-purple-500/40 transition-all duration-300'
|
||||||
</Avatar>
|
)}>
|
||||||
)}
|
<AvatarFallback className={cn(
|
||||||
|
'flex h-full w-full items-center justify-center rounded-full text-xs font-medium',
|
||||||
|
isUser
|
||||||
|
? 'bg-secondary text-secondary-foreground'
|
||||||
|
: 'bg-gradient-to-br from-purple-300 to-purple-500 text-white'
|
||||||
|
)}>
|
||||||
|
{isUser ? 'You' : 'AI'}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 max-w-3xl w-full">
|
<div className={cn('flex flex-col gap-2', isUser ? 'items-end' : 'items-start')}>
|
||||||
<div
|
<div className={cn(
|
||||||
className={cn(
|
'relative rounded-lg px-4 py-3 text-sm transition-all duration-200 max-w-[800px]',
|
||||||
'relative rounded-2xl px-4 py-3 text-sm transition-all duration-200',
|
isUser
|
||||||
message.role === 'user'
|
? 'bg-gradient-to-r from-purple-400 to-purple-500 text-white shadow-lg shadow-purple-500/20 hover:shadow-purple-500/30 hover:translate-y-[-1px]'
|
||||||
? 'bg-gradient-to-r from-purple-400 to-purple-500 text-white shadow-lg shadow-purple-500/20 hover:shadow-purple-500/30 hover:translate-y-[-1px]'
|
: 'bg-background/60 backdrop-blur-sm shadow-[0_4px_20px_-8px_rgba(0,0,0,0.1)] hover:shadow-[0_8px_30px_-12px_rgba(0,0,0,0.15)] hover:bg-background/80 hover:translate-y-[-1px]'
|
||||||
: 'bg-background/60 backdrop-blur-sm shadow-[0_4px_20px_-8px_rgba(0,0,0,0.1)] hover:shadow-[0_8px_30px_-12px_rgba(0,0,0,0.15)] hover:bg-background/80 hover:translate-y-[-1px]'
|
)}>
|
||||||
)}
|
<div className={cn(
|
||||||
>
|
'prose prose-sm max-w-none',
|
||||||
<ReactMarkdown
|
isUser ? 'prose-invert' : 'prose-neutral dark:prose-invert',
|
||||||
remarkPlugins={[remarkGfm]}
|
'prose-p:leading-relaxed prose-p:mb-2 last:prose-p:mb-0'
|
||||||
className={cn(
|
)}>
|
||||||
'prose prose-sm max-w-none',
|
<ReactMarkdown
|
||||||
'prose-headings:mb-2 prose-headings:mt-4 first:prose-headings:mt-0',
|
remarkPlugins={[remarkGfm]}
|
||||||
'prose-p:leading-relaxed prose-p:mb-2 last:prose-p:mb-0',
|
components={{
|
||||||
'prose-li:my-0',
|
pre: ({ node, ...props }) => (
|
||||||
message.role === 'user' ? 'prose-invert' : 'prose-stone dark:prose-invert'
|
<div className="overflow-auto rounded-lg bg-muted p-4">
|
||||||
)}
|
<pre {...props} />
|
||||||
components={components}
|
|
||||||
>
|
|
||||||
{finalContent}
|
|
||||||
</ReactMarkdown>
|
|
||||||
|
|
||||||
{message.role === 'assistant' && citations.length > 0 && (
|
|
||||||
<>
|
|
||||||
<Separator className="my-4 opacity-30" />
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="flex items-center gap-2 text-xs font-medium text-muted-foreground/80">
|
|
||||||
<BookOpen className="h-4 w-4" />
|
|
||||||
Sources
|
|
||||||
</div>
|
|
||||||
{citations.map((citation, index) => (
|
|
||||||
<div key={index} className="flex items-start gap-2 text-xs group/citation">
|
|
||||||
<LinkIcon className="h-3 w-3 mt-0.5 text-muted-foreground/70" />
|
|
||||||
<span className="text-muted-foreground/70 group-hover/citation:text-foreground/90 transition-colors">
|
|
||||||
{citation.text} - <span className="text-foreground/90 underline-offset-4 group-hover/citation:underline">{citation.source}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
),
|
||||||
</div>
|
code: ({ node, inline, ...props }) =>
|
||||||
</>
|
inline ? (
|
||||||
)}
|
<code className="rounded-sm bg-muted px-1 py-0.5" {...props} />
|
||||||
|
) : (
|
||||||
|
<code {...props} />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cleanContent}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
{message.role === 'assistant' && usedTools.length > 0 && (
|
{!isUser && usedTools.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Separator className="my-4 opacity-30" />
|
<Separator className="my-2 opacity-30" />
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-1 text-xs text-muted-foreground">
|
||||||
<div className="flex items-center gap-2 text-xs font-medium text-muted-foreground/80">
|
<div className="flex items-center gap-1">
|
||||||
<BookOpen className="h-4 w-4" />
|
<Wrench className="h-3 w-3" />
|
||||||
Used Tools
|
<span className="font-medium">Used Tools:</span>
|
||||||
</div>
|
</div>
|
||||||
{usedTools.map((tool, index) => (
|
{usedTools.map((tool, index) => (
|
||||||
<div key={index} className="flex items-start gap-2 text-xs">
|
<div key={index} className="flex items-center gap-1 pl-4">
|
||||||
<LinkIcon className="h-3 w-3 mt-0.5 text-muted-foreground/70" />
|
<span>{tool}</span>
|
||||||
<span className="text-muted-foreground/70">
|
|
||||||
{tool}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -234,13 +136,12 @@ export function Message({ message }: MessageProps) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 text-xs">
|
<div className={cn(
|
||||||
<span className="text-muted-foreground">
|
'flex items-center gap-2 text-xs',
|
||||||
{formatDistanceToNow(new Date(message.created_at), { addSuffix: true })}
|
'opacity-0 group-hover:opacity-100 transition-opacity duration-200'
|
||||||
</span>
|
)}>
|
||||||
|
{!isUser && (
|
||||||
{message.role === 'assistant' && (
|
<>
|
||||||
<div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
||||||
<Tooltip content="Helpful">
|
<Tooltip content="Helpful">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -267,32 +168,24 @@ export function Message({ message }: MessageProps) {
|
||||||
<ThumbsDown className="h-3 w-3" />
|
<ThumbsDown className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip content={messageCopied ? "Copied!" : "Copy message"}>
|
</>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-6 w-6 p-0 hover:text-purple-500"
|
|
||||||
onClick={handleMessageCopy}
|
|
||||||
>
|
|
||||||
{messageCopied ? (
|
|
||||||
<Check className="h-3 w-3" />
|
|
||||||
) : (
|
|
||||||
<Copy className="h-3 w-3" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
<Tooltip content={messageCopied ? "Copied!" : "Copy message"}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 w-6 p-0 hover:text-purple-500"
|
||||||
|
onClick={handleMessageCopy}
|
||||||
|
>
|
||||||
|
{messageCopied ? (
|
||||||
|
<Check className="h-3 w-3" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-3 w-3" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{message.role === 'user' && (
|
|
||||||
<Avatar className="h-8 w-8 ring-2 ring-secondary/20 transition-all duration-300 group-hover:ring-secondary/40">
|
|
||||||
<AvatarFallback className="bg-secondary text-secondary-foreground text-xs font-medium">
|
|
||||||
You
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
|
||||||
138
src/components/ui/AlertDialog.tsx
Normal file
138
src/components/ui/AlertDialog.tsx
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||||
|
import { cn } from "../../lib/utils"
|
||||||
|
|
||||||
|
const AlertDialog = AlertDialogPrimitive.Root
|
||||||
|
|
||||||
|
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||||
|
|
||||||
|
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||||
|
|
||||||
|
const AlertDialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Overlay
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const AlertDialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
))
|
||||||
|
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const AlertDialogHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-2 text-center sm:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||||
|
|
||||||
|
const AlertDialogFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||||
|
|
||||||
|
const AlertDialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-lg font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const AlertDialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
|
||||||
|
|
||||||
|
const AlertDialogAction = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Action
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-10 items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground ring-offset-background transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||||
|
|
||||||
|
const AlertDialogCancel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Cancel
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"mt-2 inline-flex h-10 items-center justify-center rounded-md border border-input bg-background px-4 py-2 text-sm font-semibold ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 sm:mt-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogPortal,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { ChatInstance, ChatMessage } from '../types/supabase';
|
import type { ChatInstance, ChatMessage, N8NChatHistory } from '../types/supabase';
|
||||||
|
import { supabase } from './supabase';
|
||||||
|
|
||||||
// Helper function to generate UUIDs
|
// Helper function to generate UUIDs
|
||||||
const generateId = () => crypto.randomUUID();
|
const generateId = () => crypto.randomUUID();
|
||||||
|
|
@ -71,6 +72,39 @@ const sendToN8N = async (sessionId: string, message: string): Promise<N8NRespons
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper function to group messages by session and create chat instances
|
||||||
|
const createChatInstanceFromMessages = (messages: N8NChatHistory[]): ChatInstance[] => {
|
||||||
|
const sessionMap = new Map<string, N8NChatHistory[]>();
|
||||||
|
|
||||||
|
// Group messages by session_id
|
||||||
|
messages.forEach(msg => {
|
||||||
|
const existing = sessionMap.get(msg.session_id) || [];
|
||||||
|
sessionMap.set(msg.session_id, [...existing, msg]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert each session group into a ChatInstance
|
||||||
|
return Array.from(sessionMap.entries()).map(([sessionId, messages]) => {
|
||||||
|
const sortedMessages = messages.sort((a, b) => a.id - b.id);
|
||||||
|
const firstMessage = sortedMessages[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: sessionId,
|
||||||
|
created_at: '',
|
||||||
|
user_id: 'system',
|
||||||
|
title: firstMessage.message.content.slice(0, 50) + '...',
|
||||||
|
last_message_at: '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ChatStats {
|
||||||
|
messageCount: number;
|
||||||
|
firstMessage: string;
|
||||||
|
lastMessage: string;
|
||||||
|
humanMessages: number;
|
||||||
|
aiMessages: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const chatService = {
|
export const chatService = {
|
||||||
async createChatInstance(userId: string, title: string): Promise<ChatInstance | null> {
|
async createChatInstance(userId: string, title: string): Promise<ChatInstance | null> {
|
||||||
try {
|
try {
|
||||||
|
|
@ -122,10 +156,14 @@ export const chatService = {
|
||||||
|
|
||||||
async getChatInstances(userId: string): Promise<ChatInstance[]> {
|
async getChatInstances(userId: string): Promise<ChatInstance[]> {
|
||||||
try {
|
try {
|
||||||
const chats = getFromStorage<ChatInstance>('chat_instances');
|
const { data: messages, error } = await supabase
|
||||||
return chats
|
.from('n8n_chat_histories')
|
||||||
.filter(chat => chat.user_id === userId)
|
.select('*')
|
||||||
.sort((a, b) => new Date(b.last_message_at).getTime() - new Date(a.last_message_at).getTime());
|
.order('id', { ascending: false });
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return createChatInstanceFromMessages(messages);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching chat instances:', error);
|
console.error('Error fetching chat instances:', error);
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -134,10 +172,26 @@ export const chatService = {
|
||||||
|
|
||||||
async getChatMessages(chatId: string): Promise<ChatMessage[]> {
|
async getChatMessages(chatId: string): Promise<ChatMessage[]> {
|
||||||
try {
|
try {
|
||||||
const messages = getFromStorage<ChatMessage>('chat_messages');
|
const { data: messages, error } = await supabase
|
||||||
return messages
|
.from('n8n_chat_histories')
|
||||||
.filter(message => message.chat_id === chatId)
|
.select('*')
|
||||||
.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
.eq('session_id', chatId)
|
||||||
|
.order('id', { ascending: true });
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
// Convert N8NChatHistory to ChatMessage format
|
||||||
|
return messages.map(msg => ({
|
||||||
|
id: msg.id.toString(),
|
||||||
|
chat_id: msg.session_id,
|
||||||
|
content: msg.message.content,
|
||||||
|
role: msg.message.type === 'human' ? 'user' : 'assistant',
|
||||||
|
created_at: new Date(msg.id).toISOString(), // Using id as timestamp approximation
|
||||||
|
metadata: {
|
||||||
|
...msg.message.response_metadata,
|
||||||
|
...msg.message.additional_kwargs
|
||||||
|
}
|
||||||
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching chat messages:', error);
|
console.error('Error fetching chat messages:', error);
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -157,7 +211,7 @@ export const chatService = {
|
||||||
chat_id: chatId,
|
chat_id: chatId,
|
||||||
content,
|
content,
|
||||||
role,
|
role,
|
||||||
created_at: new Date().toISOString(),
|
created_at: '', // Empty string for timestamp
|
||||||
metadata,
|
metadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -178,7 +232,7 @@ export const chatService = {
|
||||||
chat_id: chatId,
|
chat_id: chatId,
|
||||||
content: n8nResponse.response.content,
|
content: n8nResponse.response.content,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
created_at: new Date().toISOString(),
|
created_at: '', // Empty string for timestamp
|
||||||
metadata: {
|
metadata: {
|
||||||
...n8nResponse.response.metadata,
|
...n8nResponse.response.metadata,
|
||||||
make_response_id: userMessage.id // Link to the user's message
|
make_response_id: userMessage.id // Link to the user's message
|
||||||
|
|
@ -200,7 +254,7 @@ export const chatService = {
|
||||||
chat_id: chatId,
|
chat_id: chatId,
|
||||||
content: 'Sorry, I encountered an error processing your request.',
|
content: 'Sorry, I encountered an error processing your request.',
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
created_at: new Date().toISOString(),
|
created_at: '', // Empty string for timestamp
|
||||||
metadata: {
|
metadata: {
|
||||||
error: 'Failed to process message',
|
error: 'Failed to process message',
|
||||||
make_response_id: userMessage.id
|
make_response_id: userMessage.id
|
||||||
|
|
@ -212,17 +266,9 @@ export const chatService = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update last_message_at in chat instance
|
|
||||||
const chats = getFromStorage<ChatInstance>('chat_instances');
|
|
||||||
const chatIndex = chats.findIndex(chat => chat.id === chatId);
|
|
||||||
if (chatIndex !== -1) {
|
|
||||||
chats[chatIndex].last_message_at = new Date().toISOString();
|
|
||||||
setInStorage('chat_instances', chats);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return all messages for this chat
|
// Return all messages for this chat
|
||||||
const updatedMessages = messages.filter(msg => msg.chat_id === chatId)
|
const updatedMessages = messages.filter(msg => msg.chat_id === chatId)
|
||||||
.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
.sort((a, b) => messages.indexOf(a) - messages.indexOf(b)); // Sort by order of addition
|
||||||
console.log('Returning updated messages:', updatedMessages);
|
console.log('Returning updated messages:', updatedMessages);
|
||||||
return updatedMessages;
|
return updatedMessages;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -231,18 +277,40 @@ export const chatService = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getChatStats(chatId: string): Promise<ChatStats | null> {
|
||||||
|
try {
|
||||||
|
const { data: messages, error } = await supabase
|
||||||
|
.from('n8n_chat_histories')
|
||||||
|
.select('*')
|
||||||
|
.eq('session_id', chatId)
|
||||||
|
.order('id', { ascending: true });
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
if (!messages || messages.length === 0) return null;
|
||||||
|
|
||||||
|
const stats: ChatStats = {
|
||||||
|
messageCount: messages.length,
|
||||||
|
firstMessage: messages[0].message.content,
|
||||||
|
lastMessage: messages[messages.length - 1].message.content,
|
||||||
|
humanMessages: messages.filter(m => m.message.type === 'human').length,
|
||||||
|
aiMessages: messages.filter(m => m.message.type === 'ai').length
|
||||||
|
};
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting chat stats:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async deleteChatInstance(chatId: string): Promise<boolean> {
|
async deleteChatInstance(chatId: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
// Delete all messages for this chat
|
const { error } = await supabase
|
||||||
const messages = getFromStorage<ChatMessage>('chat_messages');
|
.from('n8n_chat_histories')
|
||||||
const filteredMessages = messages.filter(message => message.chat_id !== chatId);
|
.delete()
|
||||||
setInStorage('chat_messages', filteredMessages);
|
.eq('session_id', chatId);
|
||||||
|
|
||||||
// Delete the chat instance
|
|
||||||
const chats = getFromStorage<ChatInstance>('chat_instances');
|
|
||||||
const filteredChats = chats.filter(chat => chat.id !== chatId);
|
|
||||||
setInStorage('chat_instances', filteredChats);
|
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting chat instance:', error);
|
console.error('Error deleting chat instance:', error);
|
||||||
|
|
|
||||||
|
|
@ -57,23 +57,42 @@ export default function AskQuestion() {
|
||||||
try {
|
try {
|
||||||
// Don't try to load chat if we're using a temporary ID
|
// Don't try to load chat if we're using a temporary ID
|
||||||
if (chatId.startsWith('temp-')) {
|
if (chatId.startsWith('temp-')) {
|
||||||
|
// For new chats, create a temporary chat instance
|
||||||
|
const tempChat: ChatInstance = {
|
||||||
|
id: chatId,
|
||||||
|
created_at: '',
|
||||||
|
user_id: 'system',
|
||||||
|
title: 'New Chat',
|
||||||
|
last_message_at: '',
|
||||||
|
};
|
||||||
|
setChat(tempChat);
|
||||||
|
setMessages([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load chat instance
|
// Load messages for existing chats
|
||||||
const chatInstance = await chatService.getChatInstance(chatId);
|
|
||||||
if (!chatInstance) {
|
|
||||||
setError('Chat not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setChat(chatInstance);
|
|
||||||
|
|
||||||
// Load messages
|
|
||||||
const messages = await chatService.getChatMessages(chatId);
|
const messages = await chatService.getChatMessages(chatId);
|
||||||
setMessages(messages);
|
setMessages(messages);
|
||||||
|
|
||||||
|
// For existing chats, either load from storage or create from messages
|
||||||
|
const existingChat = await chatService.getChatInstance(chatId);
|
||||||
|
if (existingChat) {
|
||||||
|
setChat(existingChat);
|
||||||
|
} else if (messages.length > 0) {
|
||||||
|
// Create a chat instance from the first message
|
||||||
|
const firstMessage = messages[0];
|
||||||
|
const chatInstance: ChatInstance = {
|
||||||
|
id: chatId,
|
||||||
|
created_at: '',
|
||||||
|
user_id: 'system',
|
||||||
|
title: firstMessage.content.slice(0, 50) + '...',
|
||||||
|
last_message_at: '',
|
||||||
|
};
|
||||||
|
setChat(chatInstance);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError('Failed to load chat');
|
|
||||||
console.error('Error loading chat:', err);
|
console.error('Error loading chat:', err);
|
||||||
|
setError('Failed to load chat');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -109,18 +128,11 @@ export default function AskQuestion() {
|
||||||
try {
|
try {
|
||||||
let currentChatId: string;
|
let currentChatId: string;
|
||||||
|
|
||||||
// If no chat exists or we're on the base ask page, create a new one
|
// If no chat exists or we're starting a new chat, generate a new session ID
|
||||||
if (!chatId || chatId === undefined) {
|
if (!chatId || chatId.startsWith('temp-')) {
|
||||||
const title = generateChatTitle(question.trim());
|
currentChatId = crypto.randomUUID();
|
||||||
const newChat = await chatService.createChatInstance(user.id, title);
|
|
||||||
if (!newChat) {
|
|
||||||
throw new Error('Failed to create chat instance');
|
|
||||||
}
|
|
||||||
currentChatId = newChat.id;
|
|
||||||
setChat(newChat);
|
|
||||||
setIsNewChat(false);
|
|
||||||
// Update URL with the real chat ID
|
// Update URL with the real chat ID
|
||||||
navigate(`/dashboard/ask/${newChat.id}`, { replace: true });
|
navigate(`/dashboard/ask/${currentChatId}`, { replace: true });
|
||||||
} else {
|
} else {
|
||||||
currentChatId = chatId;
|
currentChatId = chatId;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,55 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { MessageSquare, Trash2 } from 'lucide-react';
|
import { MessageSquare, Trash2, User, Bot } from 'lucide-react';
|
||||||
import { Button } from '../../components/ui/Button';
|
import { Button } from '../../components/ui/Button';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
import { chatService } from '../../lib/chat-service';
|
import { chatService } from '../../lib/chat-service';
|
||||||
import type { ChatInstance } from '../../types/supabase';
|
import type { ChatInstance } from '../../types/supabase';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "../../components/ui/AlertDialog";
|
||||||
|
|
||||||
|
interface ChatWithStats extends ChatInstance {
|
||||||
|
stats?: {
|
||||||
|
messageCount: number;
|
||||||
|
humanMessages: number;
|
||||||
|
aiMessages: number;
|
||||||
|
};
|
||||||
|
isLoadingStats?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export default function StudyHistory() {
|
export default function StudyHistory() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [chats, setChats] = useState<ChatInstance[]>([]);
|
const [chats, setChats] = useState<ChatWithStats[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [chatToDelete, setChatToDelete] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
const loadChats = async () => {
|
const loadChats = async () => {
|
||||||
const chatInstances = await chatService.getChatInstances(user.id);
|
const chatInstances = await chatService.getChatInstances(user.id);
|
||||||
setChats(chatInstances);
|
setChats(chatInstances.map(chat => ({ ...chat, isLoadingStats: true })));
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
|
// Load stats for each chat
|
||||||
|
for (const chat of chatInstances) {
|
||||||
|
const stats = await chatService.getChatStats(chat.id);
|
||||||
|
setChats(prev =>
|
||||||
|
prev.map(c =>
|
||||||
|
c.id === chat.id
|
||||||
|
? { ...c, stats, isLoadingStats: false }
|
||||||
|
: c
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadChats();
|
loadChats();
|
||||||
|
|
@ -28,6 +60,7 @@ export default function StudyHistory() {
|
||||||
if (success) {
|
if (success) {
|
||||||
setChats(prev => prev.filter(chat => chat.id !== chatId));
|
setChats(prev => prev.filter(chat => chat.id !== chatId));
|
||||||
}
|
}
|
||||||
|
setChatToDelete(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
|
@ -70,13 +103,32 @@ export default function StudyHistory() {
|
||||||
{chats.map((chat) => (
|
{chats.map((chat) => (
|
||||||
<div
|
<div
|
||||||
key={chat.id}
|
key={chat.id}
|
||||||
className="group relative rounded-lg border bg-card p-4 transition-colors hover:bg-purple-50/50"
|
className="group relative rounded-lg border bg-card p-6 transition-all hover:border-purple-300 hover:shadow-md"
|
||||||
>
|
>
|
||||||
<Link to={`/dashboard/ask/${chat.id}`} className="block">
|
<Link to={`/dashboard/ask/${chat.id}`} className="block">
|
||||||
<h3 className="font-medium">{chat.title}</h3>
|
<h3 className="font-medium line-clamp-2">{chat.title}</h3>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
|
||||||
Last updated: {new Date(chat.last_message_at).toLocaleString()}
|
{chat.isLoadingStats ? (
|
||||||
</p>
|
<div className="mt-3 animate-pulse space-y-2">
|
||||||
|
<div className="h-2 w-3/4 rounded bg-gray-200"></div>
|
||||||
|
<div className="h-2 w-1/2 rounded bg-gray-200"></div>
|
||||||
|
</div>
|
||||||
|
) : chat.stats ? (
|
||||||
|
<div className="mt-3 flex items-center space-x-4 text-sm text-muted-foreground">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<MessageSquare className="mr-1 h-4 w-4" />
|
||||||
|
{chat.stats.messageCount}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<User className="mr-1 h-4 w-4" />
|
||||||
|
{chat.stats.humanMessages}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Bot className="mr-1 h-4 w-4" />
|
||||||
|
{chat.stats.aiMessages}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</Link>
|
</Link>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -84,7 +136,7 @@ export default function StudyHistory() {
|
||||||
className="absolute right-2 top-2 opacity-0 transition-opacity group-hover:opacity-100"
|
className="absolute right-2 top-2 opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleDelete(chat.id);
|
setChatToDelete(chat.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4 text-muted-foreground hover:text-destructive" />
|
<Trash2 className="h-4 w-4 text-muted-foreground hover:text-destructive" />
|
||||||
|
|
@ -93,6 +145,26 @@ export default function StudyHistory() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<AlertDialog open={!!chatToDelete} onOpenChange={() => setChatToDelete(null)}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This action cannot be undone. This will permanently delete the chat and all its messages.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
className="bg-red-500 hover:bg-red-600"
|
||||||
|
onClick={() => chatToDelete && handleDelete(chatToDelete)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
export interface ChatInstance {
|
export interface ChatInstance {
|
||||||
id: string;
|
id: string;
|
||||||
created_at: string;
|
created_at: string; // Keep this as it's required by Supabase but we'll use empty string
|
||||||
user_id: string;
|
user_id: string;
|
||||||
title: string;
|
title: string;
|
||||||
last_message_at: string;
|
last_message_at: string; // Keep this as it's required by Supabase but we'll use empty string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatMessage {
|
export interface ChatMessage {
|
||||||
|
|
@ -11,13 +11,26 @@ export interface ChatMessage {
|
||||||
chat_id: string;
|
chat_id: string;
|
||||||
content: string;
|
content: string;
|
||||||
role: 'user' | 'assistant';
|
role: 'user' | 'assistant';
|
||||||
created_at: string;
|
created_at: string; // Keep this as it's required by Supabase but we'll use empty string
|
||||||
metadata?: {
|
metadata?: {
|
||||||
make_response_id?: string;
|
make_response_id?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface N8NChatHistory {
|
||||||
|
id: number;
|
||||||
|
session_id: string;
|
||||||
|
message: {
|
||||||
|
type: 'human' | 'ai';
|
||||||
|
content: string;
|
||||||
|
tool_calls?: any[];
|
||||||
|
additional_kwargs: Record<string, any>;
|
||||||
|
response_metadata: Record<string, any>;
|
||||||
|
invalid_tool_calls?: any[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export type Database = {
|
export type Database = {
|
||||||
public: {
|
public: {
|
||||||
Tables: {
|
Tables: {
|
||||||
|
|
@ -31,6 +44,11 @@ export type Database = {
|
||||||
Insert: Omit<ChatMessage, 'id' | 'created_at'>;
|
Insert: Omit<ChatMessage, 'id' | 'created_at'>;
|
||||||
Update: Partial<Omit<ChatMessage, 'id' | 'created_at' | 'chat_id'>>;
|
Update: Partial<Omit<ChatMessage, 'id' | 'created_at' | 'chat_id'>>;
|
||||||
};
|
};
|
||||||
|
n8n_chat_histories: {
|
||||||
|
Row: N8NChatHistory;
|
||||||
|
Insert: Omit<N8NChatHistory, 'id'>;
|
||||||
|
Update: Partial<Omit<N8NChatHistory, 'id'>>;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
import forms from '@tailwindcss/forms'
|
import forms from '@tailwindcss/forms'
|
||||||
import scrollbarHide from 'tailwind-scrollbar-hide'
|
import scrollbarHide from 'tailwind-scrollbar-hide'
|
||||||
|
import lineClamp from '@tailwindcss/line-clamp'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
darkMode: ["class"],
|
darkMode: ["class"],
|
||||||
|
|
@ -75,5 +76,5 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [forms, scrollbarHide],
|
plugins: [forms, scrollbarHide, lineClamp],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue