chore: mini-openclaw 初始版本

Next.js 前端 + FastAPI/LangChain 后端透明 agent平台
- 含 .gitignore, 敏感文件(sessions数据/.env/venv等)已排除
- 待后续添加 trajectory 自进化机制
This commit is contained in:
NathanAlex7
2026-09-01 16:40:10 +08:00
committed by NathanAlex7
parent 73796a6abd
commit 42ce992a0f
76 changed files with 14207 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
"use client";
import { useState, useRef, useCallback } from "react";
import { ArrowUp, Loader2 } from "lucide-react";
import { useApp } from "@/lib/store";
export default function ChatInput() {
const [text, setText] = useState("");
const { sendMessage, isStreaming, isCompressing } = useApp();
const textareaRef = useRef<HTMLTextAreaElement>(null);
const disabled = isStreaming || isCompressing;
const handleSubmit = useCallback(() => {
if (!text.trim() || disabled) return;
sendMessage(text.trim());
setText("");
if (textareaRef.current) textareaRef.current.style.height = "auto";
}, [text, disabled, sendMessage]);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSubmit(); }
};
const handleInput = () => {
const el = textareaRef.current;
if (el) { el.style.height = "auto"; el.style.height = Math.min(el.scrollHeight, 160) + "px"; }
};
return (
<div className="p-4 pb-5">
<div className="glass-input rounded-2xl flex items-end gap-2 px-4 py-2.5 max-w-2xl mx-auto hover:shadow-md transition-shadow">
<textarea
ref={textareaRef}
value={text}
onChange={(e) => { setText(e.target.value); handleInput(); }}
onKeyDown={handleKeyDown}
placeholder="输入消息..."
rows={1}
className="flex-1 resize-none bg-transparent text-[14px] outline-none placeholder:text-gray-400 max-h-40 py-1 leading-relaxed"
/>
<button
onClick={handleSubmit}
disabled={!text.trim() || disabled}
className="shrink-0 w-8 h-8 flex items-center justify-center rounded-xl bg-[#002fa7] text-white disabled:opacity-25 hover:bg-[#001f7a] transition-all active:scale-95"
>
{disabled ? <Loader2 className="w-4 h-4 animate-spin" /> : <ArrowUp className="w-4 h-4" />}
</button>
</div>
<p className="text-center text-[10px] text-gray-400/70 mt-2">
Powered by DeepSeek · mini OpenClaw v0.1
</p>
</div>
);
}
+125
View File
@@ -0,0 +1,125 @@
"use client";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { User, AlertTriangle, Key } from "lucide-react";
import type { ChatMessage as ChatMessageType } from "@/lib/store";
import ThoughtChain from "./ThoughtChain";
import RetrievalCard from "./RetrievalCard";
interface Props {
message: ChatMessageType;
}
function formatTime(ts: number): string {
const d = new Date(ts);
return d.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
}
/** Detect 401 / API key errors */
function isAuthError(content: string): boolean {
return /401|authentication.?fail|invalid.*api.?key|api.?key.*invalid/i.test(content);
}
export default function ChatMessage({ message }: Props) {
const isUser = message.role === "user";
const hasAuthError = !isUser && isAuthError(message.content);
return (
<div className="animate-fade-in px-4 py-1.5">
<div className="max-w-2xl mx-auto">
{/* User message — right-aligned bubble */}
{isUser ? (
<div className="flex justify-end gap-2">
<div>
<div className="bg-[#002fa7] text-white px-4 py-2.5 rounded-2xl rounded-tr-md text-[14px] leading-relaxed shadow-sm">
{message.content}
</div>
<div className="text-[10px] text-gray-400 mt-1 text-right pr-1">
{formatTime(message.timestamp)}
</div>
</div>
<div className="shrink-0 w-7 h-7 rounded-full bg-gray-200 flex items-center justify-center mt-0.5">
<User className="w-3.5 h-3.5 text-gray-500" />
</div>
</div>
) : (
/* Assistant message — left-aligned */
<div className="flex gap-2.5">
<div className="shrink-0 w-7 h-7 rounded-full bg-gradient-to-br from-[#002fa7] to-[#4070ff] flex items-center justify-center mt-0.5 shadow-sm">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none">
<path d="M12 2L2 7L12 12L22 7L12 2Z" fill="white" fillOpacity="0.9" />
<path d="M2 17L12 22L22 17" stroke="white" strokeOpacity="0.7" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M2 12L12 17L22 12" stroke="white" strokeOpacity="0.85" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
<div className="flex-1 min-w-0">
{/* Tool calls */}
{message.toolCalls && message.toolCalls.length > 0 && (
<ThoughtChain toolCalls={message.toolCalls} />
)}
{/* Auth error alert */}
{hasAuthError ? (
<AuthErrorAlert content={message.content} />
) : message.content ? (
<div>
<div className="bg-white/60 px-4 py-2.5 rounded-2xl rounded-tl-md text-[14px] leading-relaxed shadow-[0_1px_3px_rgba(0,0,0,0.04)] border border-black/[0.04]">
<div className="markdown-content">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{message.content}
</ReactMarkdown>
</div>
</div>
{message.retrievals && message.retrievals.length > 0 && (
<RetrievalCard retrievals={message.retrievals} />
)}
<div className="text-[10px] text-gray-400 mt-1 pl-1">
{formatTime(message.timestamp)}
</div>
</div>
) : (
/* Typing indicator */
<div className="bg-white/60 px-4 py-3 rounded-2xl rounded-tl-md inline-flex items-center gap-1.5 shadow-[0_1px_3px_rgba(0,0,0,0.04)] border border-black/[0.04]">
<span className="typing-dot w-1.5 h-1.5 bg-[#002fa7] rounded-full" />
<span className="typing-dot w-1.5 h-1.5 bg-[#002fa7] rounded-full" />
<span className="typing-dot w-1.5 h-1.5 bg-[#002fa7] rounded-full" />
</div>
)}
</div>
</div>
)}
</div>
</div>
);
}
/** Prominent auth error alert with setup guidance */
function AuthErrorAlert({ content }: { content: string }) {
return (
<div className="animate-fade-in-scale rounded-xl border border-red-200 bg-red-50/80 px-4 py-3 space-y-2">
<div className="flex items-center gap-2">
<AlertTriangle className="w-4 h-4 text-red-500 shrink-0" />
<span className="text-[13px] font-semibold text-red-700">
API Key 认证失败
</span>
</div>
<p className="text-[12px] text-red-600/80 leading-relaxed">
你的 API Key 无效或未配置。请检查 <code className="bg-red-100 px-1 rounded text-red-700">backend/.env</code> 文件中的配置。
</p>
<div className="flex items-center gap-3 pt-1">
<a
href="http://localhost:8002/"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-[11px] font-medium text-red-600 hover:text-red-800 transition-colors"
>
<Key className="w-3 h-3" />
检查后端状态
</a>
<span className="text-[10px] text-red-400">|</span>
<span className="text-[10px] text-red-500 font-mono">{content.slice(0, 120)}...</span>
</div>
</div>
);
}
+62
View File
@@ -0,0 +1,62 @@
"use client";
import { useEffect, useRef } from "react";
import { useApp } from "@/lib/store";
import ChatMessage from "./ChatMessage";
import ChatInput from "./ChatInput";
import { Sparkles } from "lucide-react";
export default function ChatPanel() {
const { messages } = useApp();
const bottomRef = useRef<HTMLDivElement>(null);
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
return (
<div className="flex flex-col h-full">
<div className="flex-1 overflow-y-auto scroll-shadow">
{messages.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full">
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#002fa7] to-[#4070ff] flex items-center justify-center mb-4 shadow-lg shadow-[#002fa7]/15">
<Sparkles className="w-7 h-7 text-white" />
</div>
<h2 className="text-lg font-semibold text-gray-800 mb-1">
Hi, how can I help?
</h2>
<p className="text-[13px] text-gray-400 max-w-xs text-center leading-relaxed">
Ask me anything, or try{" "}
<span className="text-[#ff6723] font-medium">&quot;查询北京天气&quot;</span>
</p>
<div className="flex flex-wrap gap-2 mt-5 max-w-md justify-center">
{["你好,介绍一下自己", "查询北京天气", "帮我写一段Python代码"].map((hint) => (
<QuickHint key={hint} text={hint} />
))}
</div>
</div>
) : (
<div className="py-4">
{messages.map((msg) => (
<ChatMessage key={msg.id} message={msg} />
))}
<div ref={bottomRef} />
</div>
)}
</div>
<ChatInput />
</div>
);
}
function QuickHint({ text }: { text: string }) {
const { sendMessage, isStreaming } = useApp();
return (
<button
onClick={() => !isStreaming && sendMessage(text)}
className="px-3 py-1.5 rounded-full text-[12px] text-gray-500 bg-white/60 border border-black/[0.04] hover:bg-white hover:shadow-sm hover:text-gray-700 transition-all"
>
{text}
</button>
);
}
+54
View File
@@ -0,0 +1,54 @@
"use client";
import { useState } from "react";
import { ChevronDown, ChevronRight, Database } from "lucide-react";
import type { RetrievalResult } from "@/lib/store";
interface Props {
retrievals: RetrievalResult[];
}
export default function RetrievalCard({ retrievals }: Props) {
const [expanded, setExpanded] = useState(false);
if (retrievals.length === 0) return null;
return (
<div className="mt-1.5 mb-2 rounded-xl border border-violet-200/60 bg-violet-50/40 overflow-hidden animate-fade-in-scale">
<button
onClick={() => setExpanded((v) => !v)}
className="w-full flex items-center gap-2 px-3 py-1.5 text-[12px] hover:bg-violet-50/60 transition-colors"
>
{expanded ? (
<ChevronDown className="w-3 h-3 text-violet-400" />
) : (
<ChevronRight className="w-3 h-3 text-violet-400" />
)}
<div className="w-5 h-5 rounded flex items-center justify-center bg-violet-100">
<Database className="w-3 h-3 text-violet-600" />
</div>
<span className="font-medium text-violet-700">Memory Retrieval</span>
<span className="text-[10px] text-violet-400 ml-1">
{retrievals.length} snippet{retrievals.length > 1 ? "s" : ""}
</span>
</button>
{expanded && (
<div className="px-3 pb-2.5 space-y-2 border-t border-violet-100/60 pt-2">
{retrievals.map((r, idx) => (
<div key={idx} className="space-y-0.5">
<div className="flex items-center gap-2 text-[10px]">
<span className="font-semibold text-violet-500 uppercase tracking-wider">
{r.source}
</span>
<span className="text-violet-400">score: {r.score}</span>
</div>
<pre className="p-2 bg-white/70 rounded-lg text-[11px] text-gray-600 font-mono whitespace-pre-wrap leading-relaxed max-h-32 overflow-y-auto border border-violet-100/40">
{r.text}
</pre>
</div>
))}
</div>
)}
</div>
);
}
+80
View File
@@ -0,0 +1,80 @@
"use client";
import { useState } from "react";
import {
ChevronDown, ChevronRight, Terminal, Code, Globe,
FileText, Search, Loader2, CheckCircle2,
} from "lucide-react";
import type { ToolCall } from "@/lib/store";
const TOOL_META: Record<string, { icon: React.ElementType; color: string; bg: string }> = {
terminal: { icon: Terminal, color: "#6b7280", bg: "#f3f4f6" },
python_repl: { icon: Code, color: "#2563eb", bg: "#eff6ff" },
fetch_url: { icon: Globe, color: "#059669", bg: "#ecfdf5" },
read_file: { icon: FileText, color: "#d97706", bg: "#fffbeb" },
search_knowledge_base: { icon: Search, color: "#7c3aed", bg: "#f5f3ff" },
};
interface Props { toolCalls: ToolCall[] }
export default function ThoughtChain({ toolCalls }: Props) {
const [expanded, setExpanded] = useState<Record<number, boolean>>({});
if (toolCalls.length === 0) return null;
return (
<div className="mb-2 space-y-1">
{toolCalls.map((tc, idx) => {
const meta = TOOL_META[tc.tool] || TOOL_META.terminal;
const Icon = meta.icon;
const isOpen = expanded[idx] ?? false;
return (
<div key={idx} className="rounded-xl border border-black/[0.04] bg-white/50 overflow-hidden animate-fade-in-scale">
<button
onClick={() => setExpanded((p) => ({ ...p, [idx]: !p[idx] }))}
className="w-full flex items-center gap-2 px-3 py-1.5 text-[12px] hover:bg-black/[0.02] transition-colors"
>
{isOpen
? <ChevronDown className="w-3 h-3 text-gray-400" />
: <ChevronRight className="w-3 h-3 text-gray-400" />
}
<div
className="w-5 h-5 rounded flex items-center justify-center"
style={{ background: meta.bg }}
>
<Icon className="w-3 h-3" style={{ color: meta.color }} />
</div>
<span className="font-medium text-gray-600">{tc.tool}</span>
<span className="ml-auto">
{tc.status === "running"
? <Loader2 className="w-3.5 h-3.5 text-amber-500 animate-spin" />
: <CheckCircle2 className="w-3.5 h-3.5 text-emerald-500" />
}
</span>
</button>
{isOpen && (
<div className="px-3 pb-2 text-[11px] space-y-1.5 border-t border-black/[0.03] pt-1.5">
{tc.input && (
<div>
<span className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">Input</span>
<pre className="mt-0.5 p-2 bg-gray-50/80 rounded-lg overflow-x-auto whitespace-pre-wrap text-gray-600 font-mono leading-relaxed">
{tc.input}
</pre>
</div>
)}
{tc.output && (
<div>
<span className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">Output</span>
<pre className="mt-0.5 p-2 bg-gray-50/80 rounded-lg overflow-x-auto whitespace-pre-wrap text-gray-600 font-mono max-h-36 overflow-y-auto leading-relaxed">
{tc.output}
</pre>
</div>
)}
</div>
)}
</div>
);
})}
</div>
);
}