Skip to content

Commit 4b2f972

Browse files
Spectualclaude
andcommitted
合并创意 UI 升级:Matrix雨、Boot序列、打字机、神经网络动画
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2 parents 46a8a00 + 468334f commit 4b2f972

File tree

10 files changed

+1034
-364
lines changed

10 files changed

+1034
-364
lines changed

src/components/BootSequence.tsx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { useState, useEffect } from "react";
2+
3+
const BOOT_LINES = [
4+
{ text: "[ OK ] Starting neural network...", color: "var(--term-green)" },
5+
{ text: "[ OK ] Loading model weights...", color: "var(--term-green)" },
6+
{ text: "[ OK ] Initializing RAG pipeline...", color: "var(--term-green)" },
7+
{ text: "[ OK ] Mounting vector database...", color: "var(--term-green)" },
8+
{ text: "[ OK ] System ready.", color: "var(--term-green)" },
9+
{ text: "", color: "" },
10+
{ text: "Welcome to spectual.github.io", color: "var(--term-text)" },
11+
];
12+
13+
interface BootSequenceProps {
14+
onComplete: () => void;
15+
}
16+
17+
const BootSequence = ({ onComplete }: BootSequenceProps) => {
18+
const [visibleLines, setVisibleLines] = useState(0);
19+
const [fading, setFading] = useState(false);
20+
21+
useEffect(() => {
22+
const timers: ReturnType<typeof setTimeout>[] = [];
23+
24+
BOOT_LINES.forEach((_, i) => {
25+
timers.push(setTimeout(() => setVisibleLines(i + 1), i * 340 + 150));
26+
});
27+
28+
const totalTime = BOOT_LINES.length * 340 + 150;
29+
timers.push(setTimeout(() => setFading(true), totalTime + 400));
30+
timers.push(setTimeout(() => onComplete(), totalTime + 850));
31+
32+
return () => timers.forEach(clearTimeout);
33+
}, [onComplete]);
34+
35+
return (
36+
<div
37+
style={{
38+
position: "fixed",
39+
inset: 0,
40+
backgroundColor: "var(--term-bg)",
41+
zIndex: 1000,
42+
display: "flex",
43+
alignItems: "center",
44+
justifyContent: "center",
45+
opacity: fading ? 0 : 1,
46+
transition: "opacity 0.45s ease-out",
47+
pointerEvents: fading ? "none" : "auto",
48+
}}
49+
>
50+
<div style={{ fontFamily: "inherit", fontSize: "13px", minWidth: "320px" }}>
51+
{BOOT_LINES.slice(0, visibleLines).map((line, i) => (
52+
<div
53+
key={i}
54+
style={{
55+
color: line.color || "transparent",
56+
marginBottom: line.text ? "5px" : "14px",
57+
animation: "fadeInLine 0.18s ease-out",
58+
}}
59+
>
60+
{line.text || "\u00a0"}
61+
</div>
62+
))}
63+
{visibleLines > 0 && visibleLines <= BOOT_LINES.length && (
64+
<span className="cursor-blink" />
65+
)}
66+
</div>
67+
</div>
68+
);
69+
};
70+
71+
export default BootSequence;

src/components/ChatSection.tsx

Lines changed: 78 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,45 @@ const PREDEFINED_QUESTIONS = [
2020
"Any patents or publications?",
2121
];
2222

23+
// ── Neural network thinking indicator ─────────────────────────────────────
24+
const NN_FRAMES = [
25+
"[●○○] ──▶ [○○] ──▶ [○]",
26+
"[○●○] ──▶ [○○] ──▶ [○]",
27+
"[○○●] ──▶ [○○] ──▶ [○]",
28+
"[○○○] ──▶ [●○] ──▶ [○]",
29+
"[○○○] ──▶ [○●] ──▶ [○]",
30+
"[○○○] ──▶ [○○] ──▶ [●]",
31+
];
32+
33+
const ThinkingIndicator = () => {
34+
const [frame, setFrame] = useState(0);
35+
36+
useEffect(() => {
37+
const id = setInterval(() => setFrame((f) => (f + 1) % NN_FRAMES.length), 180);
38+
return () => clearInterval(id);
39+
}, []);
40+
41+
return (
42+
<div style={{ display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" }}>
43+
<span style={{ color: "var(--term-blue)", fontSize: "11px", flexShrink: 0 }}>ai</span>
44+
<span style={{ color: "var(--term-dim)" }}>: </span>
45+
<span
46+
style={{
47+
color: "var(--term-dim)",
48+
fontSize: "11px",
49+
fontFamily: "inherit",
50+
letterSpacing: "0.5px",
51+
}}
52+
>
53+
{NN_FRAMES[frame]}
54+
</span>
55+
<span style={{ color: "var(--term-green)", fontSize: "12px" }}>thinking</span>
56+
<span className="cursor-blink" />
57+
</div>
58+
);
59+
};
60+
61+
// ── ChatSection ────────────────────────────────────────────────────────────
2362
const ChatSection = () => {
2463
const [messages, setMessages] = useState<Message[]>([
2564
{
@@ -32,6 +71,7 @@ const ChatSection = () => {
3271
const [inputValue, setInputValue] = useState("");
3372
const [isLoading, setIsLoading] = useState(false);
3473
const [isServerOnline, setIsServerOnline] = useState(true);
74+
const [typingMessageId, setTypingMessageId] = useState<string | null>(null);
3575
const messagesEndRef = useRef<HTMLDivElement>(null);
3676
const isInitialLoad = useRef(true);
3777
const scrollContainerRef = useRef<HTMLDivElement>(null);
@@ -76,6 +116,8 @@ const ChatSection = () => {
76116
toast.error("AI server is currently offline. Please try again later.");
77117
return;
78118
}
119+
// Finish any ongoing typewriter immediately
120+
setTypingMessageId(null);
79121

80122
const userMessage: Message = {
81123
id: Date.now().toString(),
@@ -91,8 +133,9 @@ const ChatSection = () => {
91133

92134
try {
93135
const response = await sendMessage(messageText);
136+
const aiId = (Date.now() + 1).toString();
94137
const aiMessage: Message = {
95-
id: (Date.now() + 1).toString(),
138+
id: aiId,
96139
text: response.success
97140
? response.response
98141
: response.response || "Sorry, I'm having trouble connecting right now. Please try again later.",
@@ -101,6 +144,7 @@ const ChatSection = () => {
101144
};
102145
if (!response.success) toast.error("Failed to get response. Please try again.");
103146
setMessages((prev) => [...prev, aiMessage]);
147+
setTypingMessageId(aiId);
104148
} catch (error) {
105149
console.error("Error getting AI response:", error);
106150
toast.error("Failed to get response. Please try again later.");
@@ -127,10 +171,10 @@ const ChatSection = () => {
127171
};
128172

129173
return (
130-
<section className="px-4 pb-6">
174+
<section className="px-4 pb-6" style={{ position: "relative", zIndex: 1 }}>
131175
<div className="max-w-4xl mx-auto">
132176
<div style={{ border: "1px solid var(--term-border)", backgroundColor: "var(--term-bg)" }}>
133-
{/* Terminal title bar */}
177+
{/* Terminal title bar — enhanced AI style */}
134178
<div
135179
style={{
136180
borderBottom: "1px solid var(--term-border)",
@@ -139,12 +183,25 @@ const ChatSection = () => {
139183
display: "flex",
140184
alignItems: "center",
141185
justifyContent: "space-between",
186+
flexWrap: "wrap",
187+
gap: "6px",
142188
}}
143189
>
144190
<span style={{ color: "var(--term-dim)", fontSize: "12px" }}>
145-
bash — ~/chat
191+
🧠{" "}
192+
<span style={{ color: "var(--term-text)" }}>AI Assistant v2.0</span>
193+
<span style={{ color: "var(--term-dim)" }}>
194+
{" "}| model:{" "}
195+
</span>
196+
<span style={{ color: "var(--term-cyan)" }}>rag-powered</span>
197+
<span style={{ color: "var(--term-dim)" }}>
198+
{" "}| status:{" "}
199+
</span>
200+
<span style={{ color: isServerOnline ? "var(--term-green)" : "var(--term-red)" }}>
201+
{isServerOnline ? "online" : "offline"}
202+
</span>
146203
</span>
147-
{/* Server status */}
204+
{/* Server status dot */}
148205
<span
149206
style={{
150207
fontSize: "11px",
@@ -161,6 +218,7 @@ const ChatSection = () => {
161218
borderRadius: "50%",
162219
backgroundColor: isServerOnline ? "var(--term-green)" : "var(--term-red)",
163220
display: "inline-block",
221+
animation: isServerOnline ? "neuralPulse 2s ease-in-out infinite" : "none",
164222
}}
165223
/>
166224
{isServerOnline ? "server:online" : "server:offline"}
@@ -212,18 +270,14 @@ const ChatSection = () => {
212270
}}
213271
>
214272
{messages.map((message) => (
215-
<MessageBubble key={message.id} message={message} />
273+
<MessageBubble
274+
key={message.id}
275+
message={message}
276+
isTyping={typingMessageId === message.id}
277+
onTypingComplete={() => setTypingMessageId(null)}
278+
/>
216279
))}
217-
{isLoading && (
218-
<div style={{ display: "flex", alignItems: "center", gap: "6px", color: "var(--term-dim)" }}>
219-
<span style={{ color: "var(--term-blue)" }}>ai</span>
220-
<span style={{ color: "var(--term-dim)" }}>: </span>
221-
<span style={{ color: "var(--term-green)" }}>
222-
thinking
223-
<span className="cursor-blink" />
224-
</span>
225-
</div>
226-
)}
280+
{isLoading && <ThinkingIndicator />}
227281
<div ref={messagesEndRef} />
228282
</div>
229283

@@ -261,11 +315,13 @@ const ChatSection = () => {
261315
if (!isLoading && isServerOnline) {
262316
e.currentTarget.style.borderColor = "var(--term-green)";
263317
e.currentTarget.style.color = "var(--term-green)";
318+
e.currentTarget.style.backgroundColor = "rgba(63,185,80,0.06)";
264319
}
265320
}}
266321
onMouseLeave={(e) => {
267322
e.currentTarget.style.borderColor = "var(--term-border)";
268323
e.currentTarget.style.color = "var(--term-dim)";
324+
e.currentTarget.style.backgroundColor = "transparent";
269325
}}
270326
>
271327
[{index + 1}] {question}
@@ -311,10 +367,12 @@ const ChatSection = () => {
311367
style={{
312368
background: "transparent",
313369
border: "none",
314-
color: inputValue.trim() && !isLoading && isServerOnline
315-
? "var(--term-green)"
316-
: "var(--term-border)",
317-
cursor: inputValue.trim() && !isLoading && isServerOnline ? "pointer" : "default",
370+
color:
371+
inputValue.trim() && !isLoading && isServerOnline
372+
? "var(--term-green)"
373+
: "var(--term-border)",
374+
cursor:
375+
inputValue.trim() && !isLoading && isServerOnline ? "pointer" : "default",
318376
fontFamily: "inherit",
319377
fontSize: "12px",
320378
padding: "0 4px",

src/components/MatrixRain.tsx

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { useEffect, useRef } from "react";
2+
3+
const ML_CHARS = "01∑∇θσλWbxy∂αβγπφψΩε∞≈∫∏μν∈⊕⊗‖∥";
4+
5+
const MatrixRain = () => {
6+
const canvasRef = useRef<HTMLCanvasElement>(null);
7+
8+
useEffect(() => {
9+
const canvas = canvasRef.current;
10+
if (!canvas) return;
11+
12+
const ctx = canvas.getContext("2d");
13+
if (!ctx) return;
14+
15+
// Lower density on mobile
16+
const isMobile = window.innerWidth < 768;
17+
18+
const fontSize = 14;
19+
let columns = 0;
20+
let drops: number[] = [];
21+
22+
const resize = () => {
23+
canvas.width = window.innerWidth;
24+
canvas.height = window.innerHeight;
25+
const newCols = isMobile
26+
? Math.floor(canvas.width / (fontSize * 3))
27+
: Math.floor(canvas.width / fontSize);
28+
if (newCols !== columns) {
29+
columns = newCols;
30+
drops = Array(columns).fill(1);
31+
}
32+
};
33+
resize();
34+
window.addEventListener("resize", resize);
35+
36+
let animId: number;
37+
let lastTime = 0;
38+
const frameInterval = isMobile ? 160 : 90;
39+
40+
const draw = (timestamp: number) => {
41+
animId = requestAnimationFrame(draw);
42+
if (timestamp - lastTime < frameInterval) return;
43+
lastTime = timestamp;
44+
45+
// Very slow trail fade
46+
ctx.fillStyle = "rgba(13, 17, 23, 0.06)";
47+
ctx.fillRect(0, 0, canvas.width, canvas.height);
48+
49+
ctx.fillStyle = "rgba(63, 185, 80, 0.55)";
50+
ctx.font = `${fontSize}px 'JetBrains Mono', monospace`;
51+
52+
const colSpacing = isMobile ? fontSize * 3 : fontSize;
53+
for (let i = 0; i < drops.length; i++) {
54+
const char = ML_CHARS[Math.floor(Math.random() * ML_CHARS.length)];
55+
ctx.fillText(char, i * colSpacing, drops[i] * fontSize);
56+
57+
if (drops[i] * fontSize > canvas.height && Math.random() > 0.975) {
58+
drops[i] = 0;
59+
}
60+
drops[i]++;
61+
}
62+
};
63+
64+
animId = requestAnimationFrame(draw);
65+
66+
return () => {
67+
cancelAnimationFrame(animId);
68+
window.removeEventListener("resize", resize);
69+
};
70+
}, []);
71+
72+
return (
73+
<canvas
74+
ref={canvasRef}
75+
style={{
76+
position: "fixed",
77+
top: 0,
78+
left: 0,
79+
width: "100%",
80+
height: "100%",
81+
opacity: 0.035,
82+
pointerEvents: "none",
83+
zIndex: 0,
84+
}}
85+
/>
86+
);
87+
};
88+
89+
export default MatrixRain;

0 commit comments

Comments
 (0)