Download src/components/AgentInterface.jsx from shutajb/agent: direct link, hf CLI and curl.
- Browser
- Download file 11.9 kB
-
https://huggingface.co/spaces/shutajb/agent/resolve/main/src/components/AgentInterface.jsx
- Command line
-
hf download hf://spaces/shutajb/agent/src/components/AgentInterface.jsx
-
curl -L -o AgentInterface.jsx https://huggingface.co/spaces/shutajb/agent/resolve/main/src/components/AgentInterface.jsx
11.9 kB
| import { useState } from 'react' | |
| import { Play, Brain, Plus, Trash2, Settings2, Loader2, Wrench } from 'lucide-react' | |
| import { useAgentStore } from '../store/agentStore' | |
| import { tools as builtinTools } from '../utils/tools' | |
| import { agents } from '../utils/agents' | |
| const EMPTY_PARAMS = '{"type":"object","properties":{}}' | |
| function ToolChip({ call }) { | |
| return ( | |
| <div className="mt-2 p-3 rounded-2xl bg-app border border-edge text-sm"> | |
| <div className="flex items-center gap-2 text-amber-400 font-medium"> | |
| <Wrench className="w-4 h-4" /> {call?.function?.name || '未知工具'} | |
| </div> | |
| <pre className="mt-1 text-xs text-muted whitespace-pre-wrap break-words">{call?.function?.arguments || ''}</pre> | |
| </div> | |
| ) | |
| } | |
| function ToolModal({ toolsList, onChange, onClose }) { | |
| const [draft, setDraft] = useState({ name: '', description: '', parameters: EMPTY_PARAMS }) | |
| const [err, setErr] = useState('') | |
| const addTool = () => { | |
| const name = draft.name.trim() | |
| if (!name) return setErr('請輸入工具名稱') | |
| if (!/^[a-zA-Z0-9_-]+$/.test(name)) return setErr('名稱只能用英數字、底線與連字號') | |
| if (toolsList.some((t) => t.name === name)) return setErr('名稱重複') | |
| let parameters | |
| try { | |
| parameters = JSON.parse(draft.parameters || '{}') | |
| } catch { | |
| return setErr('parameters 不是合法 JSON') | |
| } | |
| onChange([ | |
| ...toolsList, | |
| { name, description: draft.description.trim() || '自訂工具', parameters }, | |
| ]) | |
| setDraft({ name: '', description: '', parameters: EMPTY_PARAMS }) | |
| setErr('') | |
| } | |
| return ( | |
| <div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50"> | |
| <div className="bg-panel p-8 rounded-3xl w-full max-w-lg border border-edge"> | |
| <h3 className="text-2xl font-bold mb-6">管理工具</h3> | |
| <div className="space-y-3 mb-6"> | |
| {toolsList.length === 0 && <p className="text-sm text-muted">目前沒有工具</p>} | |
| {toolsList.map((t) => ( | |
| <div key={t.name} className="flex items-start justify-between gap-3 bg-field p-3 rounded-2xl"> | |
| <div className="min-w-0"> | |
| <div className="font-medium break-words">{t.name}</div> | |
| <div className="text-xs text-muted break-words">{t.description}</div> | |
| </div> | |
| <button | |
| onClick={() => onChange(toolsList.filter((x) => x.name !== t.name))} | |
| className="text-red-500 shrink-0" | |
| title="移除工具" | |
| > | |
| <Trash2 className="w-4 h-4" /> | |
| </button> | |
| </div> | |
| ))} | |
| </div> | |
| <div className="space-y-3 p-4 bg-field rounded-2xl"> | |
| <div className="text-sm font-medium">新增工具</div> | |
| <input | |
| type="text" | |
| value={draft.name} | |
| onChange={(e) => setDraft({ ...draft, name: e.target.value })} | |
| placeholder="name,例如 get_weather" | |
| className="w-full bg-app px-4 py-2 rounded-xl focus:outline-none" | |
| /> | |
| <input | |
| type="text" | |
| value={draft.description} | |
| onChange={(e) => setDraft({ ...draft, description: e.target.value })} | |
| placeholder="description(模型靠它決定何時呼叫)" | |
| className="w-full bg-app px-4 py-2 rounded-xl focus:outline-none" | |
| /> | |
| <textarea | |
| value={draft.parameters} | |
| onChange={(e) => setDraft({ ...draft, parameters: e.target.value })} | |
| placeholder='parameters,例如 {"type":"object","properties":{"city":{"type":"string"}}}' | |
| rows={3} | |
| className="w-full bg-app px-4 py-2 rounded-xl font-mono text-xs focus:outline-none" | |
| /> | |
| {err && <p className="text-xs text-red-400">{err}</p>} | |
| <p className="text-xs text-muted"> | |
| 注意:自訂工具只送進 API 讓模型呼叫,本機沒有對應的執行器,會回傳「未實作」。 | |
| </p> | |
| <button | |
| onClick={addTool} | |
| className="w-full flex items-center justify-center gap-2 bg-purple-600 py-3 rounded-2xl font-medium" | |
| > | |
| <Plus className="w-4 h-4" /> 新增工具 | |
| </button> | |
| </div> | |
| <button onClick={onClose} className="mt-6 w-full py-3 bg-field rounded-2xl">關閉</button> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| export default function AgentInterface({ apiConfig, onOpenSettings }) { | |
| const [input, setInput] = useState('') | |
| const [toolsList, setToolsList] = useState(builtinTools) | |
| const [currentAgent, setCurrentAgent] = useState(agents[0]) | |
| const [showToolModal, setShowToolModal] = useState(false) | |
| const [showAgentModal, setShowAgentModal] = useState(false) | |
| const { messages, running, step, error, runAgent, reset } = useAgentStore() | |
| const handleRun = async () => { | |
| const text = input.trim() | |
| if (!text || running) return | |
| setInput('') | |
| await runAgent(text, toolsList, apiConfig, currentAgent) | |
| } | |
| const clearAll = () => { | |
| reset() | |
| setInput('') | |
| } | |
| const ready = !!(apiConfig.apiKey?.trim() && apiConfig.supplier?.trim() && apiConfig.model?.trim()) | |
| return ( | |
| <div className="flex flex-col h-[80vh] border border-edge rounded-3xl overflow-hidden bg-panel"> | |
| <div className="p-4 border-b border-edge flex items-center justify-between gap-3 flex-wrap bg-app"> | |
| <div className="flex items-center gap-3"> | |
| <Brain className="w-6 h-6 text-purple-500" /> | |
| <div> | |
| <h2 className="text-xl font-semibold leading-tight">Agent Studio</h2> | |
| <p className="text-xs text-muted"> | |
| 自訂多步工具呼叫迴圈(最多 6 步)· 目前 Agent:{currentAgent.icon} {currentAgent.name} | |
| </p> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <button | |
| onClick={onOpenSettings} | |
| className="flex items-center gap-2 px-4 py-2 bg-field rounded-xl" | |
| title="API 連線設定" | |
| > | |
| <Settings2 className="w-4 h-4" /> | |
| <span className={`text-xs ${ready ? 'text-emerald-400' : 'text-red-400'}`}> | |
| {ready ? `${apiConfig.model.slice(0, 24)}…` : '未設定金鑰'} | |
| </span> | |
| </button> | |
| <button | |
| onClick={clearAll} | |
| disabled={running || messages.length === 0} | |
| className="flex items-center gap-2 px-4 py-2 bg-field rounded-xl disabled:opacity-40" | |
| title="清空對話" | |
| > | |
| <Trash2 className="w-4 h-4" /> 清空 | |
| </button> | |
| <button | |
| onClick={() => setShowToolModal(true)} | |
| className="flex items-center gap-2 px-4 py-2 bg-field rounded-xl" | |
| > | |
| <Plus className="w-4 h-4" /> 工具 ({toolsList.length}) | |
| </button> | |
| <button | |
| onClick={() => setShowAgentModal(true)} | |
| className="flex items-center gap-2 px-4 py-2 bg-field rounded-xl" | |
| > | |
| <Brain className="w-4 h-4" /> 切換 Agent | |
| </button> | |
| <button | |
| onClick={handleRun} | |
| disabled={running || !input.trim()} | |
| className="flex items-center gap-2 bg-purple-600 hover:bg-purple-700 px-6 py-2 rounded-2xl disabled:opacity-50" | |
| > | |
| {running ? <Loader2 className="w-4 h-4 animate-spin" /> : <Play className="w-4 h-4" />} | |
| {running ? `執行中 ${step}/6` : 'Run'} | |
| </button> | |
| </div> | |
| </div> | |
| {error && !running && ( | |
| <div className="px-4 py-2 bg-red-900/50 text-red-200 text-sm border-b border-red-900">{error}</div> | |
| )} | |
| <div className="flex-1 overflow-y-auto p-4 space-y-3"> | |
| {messages.length === 0 && !running && ( | |
| <div className="h-full flex flex-col items-center justify-center text-center text-muted gap-2"> | |
| <span className="text-4xl">{currentAgent.icon}</span> | |
| <p className="text-sm">{currentAgent.description}</p> | |
| <p className="text-xs">輸入任務後按 Enter 執行</p> | |
| </div> | |
| )} | |
| {messages.map((msg, i) => { | |
| if (msg.role === 'user') { | |
| return ( | |
| <div key={i} className="bg-blue-600 p-4 rounded-3xl mr-12 whitespace-pre-wrap break-words"> | |
| {msg.content} | |
| </div> | |
| ) | |
| } | |
| if (msg.role === 'tool') { | |
| return ( | |
| <div key={i} className="p-4 rounded-3xl bg-field border-l-4 border-amber-500 ml-12 text-sm"> | |
| <div className="flex items-center gap-2 text-amber-400 font-medium mb-1"> | |
| <Wrench className="w-4 h-4" /> {msg.name || msg.tool_call_id} | |
| </div> | |
| <div className="whitespace-pre-wrap break-words">{msg.content}</div> | |
| </div> | |
| ) | |
| } | |
| const isError = typeof msg.content === 'string' && msg.content.startsWith('⚠️') | |
| return ( | |
| <div key={i} className={`p-4 rounded-3xl ${isError ? 'bg-red-900/60 text-red-200' : 'bg-field'} ml-12`}> | |
| <div className="whitespace-pre-wrap break-words"> | |
| {typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)} | |
| </div> | |
| {(msg.tool_calls || []).map((call, j) => <ToolChip key={j} call={call} />)} | |
| </div> | |
| ) | |
| })} | |
| {running && step === 0 && ( | |
| <div className="p-4 rounded-3xl bg-field ml-12 flex items-center gap-2 text-muted text-sm"> | |
| <Loader2 className="w-4 h-4 animate-spin" /> 等待模型回應... | |
| </div> | |
| )} | |
| </div> | |
| <div className="p-4 border-t border-edge"> | |
| <input | |
| value={input} | |
| onChange={(e) => setInput(e.target.value)} | |
| onKeyDown={(e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault() | |
| handleRun() | |
| } | |
| }} | |
| disabled={running} | |
| placeholder={`輸入任務(目前使用 ${currentAgent.name})...`} | |
| className="w-full bg-field px-6 py-4 rounded-3xl focus:outline-none disabled:opacity-50" | |
| /> | |
| </div> | |
| {showToolModal && ( | |
| <ToolModal toolsList={toolsList} onChange={setToolsList} onClose={() => setShowToolModal(false)} /> | |
| )} | |
| {showAgentModal && ( | |
| <div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50"> | |
| <div className="bg-panel p-8 rounded-3xl w-full max-w-md border border-edge"> | |
| <h3 className="text-2xl font-bold mb-6">選擇 Agent</h3> | |
| <div className="space-y-3"> | |
| {agents.map((agent) => ( | |
| <button | |
| key={agent.id} | |
| onClick={() => { | |
| setCurrentAgent(agent) | |
| setShowAgentModal(false) | |
| }} | |
| className={`w-full text-left px-5 py-4 rounded-2xl flex items-center gap-3 ${ | |
| currentAgent.id === agent.id ? 'bg-purple-600' : 'bg-field hover:bg-app' | |
| }`} | |
| > | |
| <div className="text-2xl">{agent.icon}</div> | |
| <div className="min-w-0"> | |
| <div className="font-medium">{agent.name}</div> | |
| <div className="text-xs text-muted break-words">{agent.description}</div> | |
| </div> | |
| </button> | |
| ))} | |
| </div> | |
| <p className="text-xs text-muted mt-4">切換後會以該 Agent 的 system prompt 執行下一輪。</p> | |
| <button onClick={() => setShowAgentModal(false)} className="mt-6 w-full py-3 bg-field rounded-2xl"> | |
| 關閉 | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| } | |