File size: 4,728 Bytes
0c2fc21
d3c7f96
 
0c2fc21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c38a89e
0c2fc21
 
 
 
 
c38a89e
0c2fc21
 
 
 
 
 
 
 
 
 
 
 
 
 
c38a89e
0c2fc21
 
 
 
 
45f314a
d3c7f96
 
 
 
 
45f314a
 
 
 
 
 
 
 
 
 
 
d3c7f96
 
 
 
 
c38a89e
0c2fc21
c38a89e
d3c7f96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c38a89e
d3c7f96
 
 
 
 
 
 
c38a89e
d3c7f96
 
 
 
c38a89e
d3c7f96
 
 
 
 
 
c38a89e
d3c7f96
 
 
c38a89e
0c2fc21
d3c7f96
 
 
 
 
 
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
129
130
131
import { useState } from "react";
import Markdown from "./Markdown.jsx";

const THINK_START = "‹‹THINK››";
const THINK_END = "‹‹/THINK››";

function splitThinking(text) {
  const startIdx = text.indexOf(THINK_START);
  const endIdx = text.indexOf(THINK_END);

  if (startIdx !== -1 && endIdx !== -1) {
    const thinking = text.slice(startIdx + THINK_START.length, endIdx).trim();
    const response = text.slice(endIdx + THINK_END.length).trim();
    return { thinking, response };
  }
  // Still streaming inside think block
  if (startIdx !== -1 && endIdx === -1) {
    const thinking = text.slice(startIdx + THINK_START.length).trim();
    return { thinking, response: null };
  }
  return { thinking: null, response: text };
}

function ThinkingBlock({ thinking }) {
  const [open, setOpen] = useState(false);
  if (!thinking) return null;

  return (
    <div className="mb-3">
      <button
        onClick={() => setOpen((v) => !v)}
        className="flex items-center gap-1.5 text-[11px] text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors cursor-pointer"
      >
        <span className={`transition-transform ${open ? "rotate-90" : ""}`}></span>
        <span>💭 Thinking</span>
      </button>
      {open && (
        <div className="mt-1.5 pl-4 border-l-2 border-[var(--color-blue)]/20 text-xs text-[var(--color-text-secondary)]/80 leading-relaxed whitespace-pre-wrap">
          {thinking}
        </div>
      )}
    </div>
  );
}

function AssistantContent({ text }) {
  const { thinking, response } = splitThinking(text);
  return (
    <>
      <ThinkingBlock thinking={thinking} />
      {response && <Markdown>{response}</Markdown>}
      {response === null && thinking && (
        <span className="text-[11px] text-[var(--color-text-secondary)] italic">thinking...</span>
      )}
    </>
  );
}

export default function MessageList({ messages, streamingText, isStreaming, processingStep }) {
  return (
    <div className="px-4 py-4 space-y-6">
      {messages.map((msg, i) => (
        <Message key={i} msg={msg} />
      ))}
      {isStreaming && processingStep && !streamingText && (
        <div className="flex gap-3">
          <div className="w-7 h-7 rounded-lg bg-gradient-to-br from-[#3186FF] to-[#4FA0FF] flex items-center justify-center text-white text-xs font-bold shrink-0">
            G
          </div>
          <div className="flex items-center gap-2 text-sm text-[var(--color-text-secondary)] pt-1">
            <span className="inline-block w-1.5 h-1.5 rounded-full bg-[var(--color-blue)] animate-pulse" />
            <span className="capitalize">{processingStep}...</span>
          </div>
        </div>
      )}
      {isStreaming && streamingText && (
        <div className="flex gap-3">
          <div className="w-7 h-7 rounded-lg bg-gradient-to-br from-[#3186FF] to-[#4FA0FF] flex items-center justify-center text-white text-xs font-bold shrink-0">
            G
          </div>
          <div className="text-sm text-[var(--color-text)] leading-relaxed pt-1 min-w-0">
            <AssistantContent text={streamingText} />
            <span className="inline-block w-1.5 h-4 bg-[var(--color-blue)] animate-pulse ml-0.5 align-text-bottom rounded-sm" />
          </div>
        </div>
      )}
    </div>
  );
}

function Message({ msg }) {
  const isUser = msg.role === "user";

  return (
    <div className="flex gap-3">
      <div
        className={`w-7 h-7 rounded-lg flex items-center justify-center text-xs font-bold shrink-0 ${
          isUser
            ? "bg-[var(--color-surface-high)] text-[var(--color-text-secondary)]"
            : "bg-gradient-to-br from-[#3186FF] to-[#4FA0FF] text-white"
        }`}
      >
        {isUser ? "Y" : "G"}
      </div>
      <div className="flex-1 min-w-0 pt-0.5">
        {msg.videoUrl ? (
          <video controls src={msg.videoUrl} className="max-w-sm max-h-48 rounded-xl mb-2 border border-[var(--color-outline)]" />
        ) : msg.imageUrl ? (
          <img
            src={msg.imageUrl}
            alt="Attached"
            className="max-w-xs max-h-48 rounded-xl mb-2 border border-[var(--color-outline)]"
          />
        ) : null}
        {msg.audioUrl && (
          <audio controls src={msg.audioUrl} className="mb-2 h-8 max-w-xs" />
        )}
        {isUser ? (
          <div className="text-sm text-[var(--color-text)] leading-relaxed">
            {msg.content.filter((c) => c.type === "text").map((c) => c.text).join("")}
          </div>
        ) : (
          <div className="text-sm text-[var(--color-text)] leading-relaxed">
            <AssistantContent text={msg.content} />
          </div>
        )}
      </div>
    </div>
  );
}