File size: 4,434 Bytes
1904e4c
978caa8
22b1735
 
 
 
 
978caa8
 
22b1735
 
 
 
 
 
 
 
 
 
978caa8
 
 
 
22b1735
978caa8
 
1904e4c
 
978caa8
 
 
 
 
1904e4c
 
 
978caa8
 
22b1735
 
 
 
 
 
 
 
 
 
 
978caa8
22b1735
1904e4c
978caa8
 
 
 
 
1904e4c
22b1735
 
 
 
 
 
 
 
978caa8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22b1735
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128

// src/components/chat/ChatMessage.tsx
import React, { useState, useMemo } from 'react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import rehypeRaw from 'rehype-raw'
import { cn } from '@/lib/utils'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../ui/collapsible'
import { ChevronDown, Brain } from 'lucide-react'

interface ChatMessageProps {
  content: string
  className?: string
}

export const ChatMessage: React.FC<ChatMessageProps> = ({
  content,
  className,
}) => {
  // Extract thinking content and actual response
  const { processedContent, thinkingBlocks } = useMemo(() => {
    const blocks: { id: number; content: string }[] = [];
    let thinkBlockCounter = 0;

    // Extract thinking content between <think> tags
    const contentWithoutThinking = content.replace(
      /<think>([\s\S]*?)<\/think>/g,
      (_, thinkContent) => {
        blocks.push({ 
          id: thinkBlockCounter++, 
          content: thinkContent.trim() 
        });
        return ''; // Remove thinking content from the main message
      }
    );

    // Continue processing source tags
    const processedText = contentWithoutThinking.replace(
      /<source\s+path=["'](.+?)["']\s*\/>/g,
      (_match, path) => {
        const filename = path
          .split('/')
          .pop()!
          .replace(/\.[^/.]+$/, '')
        return `<a href="${path}" target="_blank" class="inline-flex items-center text-xs font-medium mx-0.5 rounded-sm px-1 bg-financial-accent/10 text-financial-accent border border-financial-accent/20 hover:bg-financial-accent/20 transition-colors">
            ${filename}
            <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
              <path stroke-linecap="round" stroke-linejoin="round" d="M18 13v6a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6m0 0v6m0-6L10 14"/>
            </svg>
          </a>`;
      }
    );

    return { 
      processedContent: processedText.trim(), 
      thinkingBlocks: blocks 
    };
  }, [content]);

  return (
    <div
      className={cn(
        'group relative w-full rounded-md p-2 hover:bg-muted/30 transition-colors',
        className
      )}
    >
      {/* First render the thinking blocks if any */}
      {thinkingBlocks.length > 0 && (
        <Collapsible 
          className="think-collapsible my-3 rounded-lg bg-financial-accent/5 mb-4"
          defaultOpen={false}
        >
          <CollapsibleTrigger className="flex items-center gap-2 w-full p-2 text-left hover:bg-financial-accent/10 rounded-t-lg">
            <div className="flex items-center gap-2 w-full">
              <div className="thinking-brain-small relative">
                <Brain className="h-4 w-4 text-financial-accent" />
              </div>
              <span className="text-xs font-medium text-financial-accent">Thoughts</span>
              <ChevronDown className="h-4 w-4 text-financial-accent/70 transition-transform duration-200 ml-auto" />
            </div>
          </CollapsibleTrigger>
          
          <CollapsibleContent>
            <div className="think-block p-3 text-sm text-muted-foreground bg-financial-accent/5">
              {thinkingBlocks.map((block, index) => (
                <ReactMarkdown 
                  key={`thinking-${block.id}`}
                  remarkPlugins={[remarkGfm]} 
                  rehypePlugins={[rehypeRaw]}
                >
                  {block.content}
                </ReactMarkdown>
              ))}
            </div>
          </CollapsibleContent>
        </Collapsible>
      )}

      {/* Then render the actual response content */}
      {processedContent && (
        <ReactMarkdown
          remarkPlugins={[remarkGfm]}
          rehypePlugins={[rehypeRaw]}
          components={{
            a: ({ href, children, node, ...props }) =>
              href && href.endsWith('.md') ? (
                <a
                  href={href}
                  target="_blank"
                  rel="noopener noreferrer"
                  {...props}
                >
                  {children}
                </a>
              ) : (
                <a href={href} {...props}>
                  {children}
                </a>
              ),
          }}
        >
          {processedContent}
        </ReactMarkdown>
      )}
    </div>
  )
}