Better UX

This commit is contained in:
Xin Wang
2026-02-04 18:36:40 +08:00
parent 47207dab19
commit b608c395c7
14 changed files with 877 additions and 403 deletions

View File

@@ -1,8 +1,8 @@
import React, { useState, useEffect, useRef } from 'react';
import { Plus, Search, Play, Copy, Trash2, Edit2, Mic, MessageSquare, Save, Video, PhoneOff, Camera, ArrowLeftRight, Send, Phone, MoreHorizontal, Rocket, AlertTriangle, PhoneCall, CameraOff, Image, Images, CloudSun, Calendar, TrendingUp, Coins, Wrench, Globe, Terminal, X } from 'lucide-react';
import { Plus, Search, Play, Copy, Trash2, Edit2, Mic, MessageSquare, Save, Video, PhoneOff, Camera, ArrowLeftRight, Send, Phone, MoreHorizontal, Rocket, AlertTriangle, PhoneCall, CameraOff, Image, Images, CloudSun, Calendar, TrendingUp, Coins, Wrench, Globe, Terminal, X, ClipboardCheck, Sparkles, Volume2, Timer, ChevronDown, Link as LinkIcon, Database, Server, Zap, ExternalLink, Key } from 'lucide-react';
import { Button, Input, Card, Badge, Drawer, Dialog } from '../components/UI';
import { mockAssistants, mockKnowledgeBases } from '../services/mockData';
import { mockAssistants, mockKnowledgeBases, mockVoices } from '../services/mockData';
import { Assistant, TabValue } from '../types';
import { GoogleGenAI } from "@google/genai";
@@ -23,9 +23,13 @@ export const AssistantsPage: React.FC = () => {
const [debugOpen, setDebugOpen] = useState(false);
const [hotwordInput, setHotwordInput] = useState('');
// Publish Modal State
const [isPublishModalOpen, setIsPublishModalOpen] = useState(false);
const [publishTab, setPublishTab] = useState<'web' | 'api'>('web');
// Custom Tools State
const [customTools, setCustomTools] = useState<ToolItem[]>([]);
const [hiddenToolIds, setHiddenToolIds] = useState<string[]>([]); // Track deleted/hidden base tools
const [hiddenToolIds, setHiddenToolIds] = useState<string[]>([]);
const [isAddToolModalOpen, setIsAddToolModalOpen] = useState(false);
const [addingToCategory, setAddingToCategory] = useState<'system' | 'query'>('system');
@@ -33,8 +37,9 @@ export const AssistantsPage: React.FC = () => {
const [newToolName, setNewToolName] = useState('');
const [newToolDesc, setNewToolDesc] = useState('');
// State for delete confirmation dialog
const [deleteId, setDeleteId] = useState<string | null>(null);
const [copySuccess, setCopySuccess] = useState(false);
const [saveLoading, setSaveLoading] = useState(false);
const selectedAssistant = assistants.find(a => a.id === selectedId) || null;
@@ -43,7 +48,7 @@ export const AssistantsPage: React.FC = () => {
);
const handleCreate = () => {
const newId = Date.now().toString();
const newId = Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
const newAssistant: Assistant = {
id: newId,
name: 'New Assistant',
@@ -52,18 +57,37 @@ export const AssistantsPage: React.FC = () => {
prompt: '',
knowledgeBaseId: '',
language: 'zh',
voice: 'default',
voice: mockVoices[0]?.id || '',
speed: 1,
hotwords: [],
tools: []
tools: [],
interruptionSensitivity: 500,
configMode: 'platform',
};
setAssistants([...assistants, newAssistant]);
setSelectedId(newId);
setActiveTab(TabValue.GLOBAL);
};
const handleSave = () => {
setSaveLoading(true);
// Simulate API call
setTimeout(() => {
setSaveLoading(false);
// In a real app, logic to persist selectedAssistant would go here
}, 800);
};
const handleCopyId = (id: string, text?: string) => {
navigator.clipboard.writeText(text || id);
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
};
const handleCopy = (e: React.MouseEvent, assistant: Assistant) => {
e.stopPropagation();
const newAssistant = { ...assistant, id: Date.now().toString(), name: `${assistant.name} (Copy)` };
const newId = Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
const newAssistant = { ...assistant, id: newId, name: `${assistant.name} (Copy)` };
setAssistants([...assistants, newAssistant]);
};
@@ -83,6 +107,14 @@ export const AssistantsPage: React.FC = () => {
const updateAssistant = (field: keyof Assistant, value: any) => {
if (!selectedId) return;
setAssistants(prev => prev.map(a => a.id === selectedId ? { ...a, [field]: value } : a));
if (field === 'configMode') {
if (value === 'platform') {
setActiveTab(TabValue.GLOBAL);
} else if (value === 'dify' || value === 'fastgpt') {
setActiveTab(TabValue.LINK);
}
}
};
const toggleTool = (toolId: string) => {
@@ -96,13 +128,11 @@ export const AssistantsPage: React.FC = () => {
const deleteTool = (e: React.MouseEvent, toolId: string) => {
e.stopPropagation();
// 1. Remove from assistant configurations if enabled
setAssistants(prev => prev.map(a => ({
...a,
tools: a.tools?.filter(id => id !== toolId) || []
})));
// 2. Remove from tool list
if (customTools.some(t => t.id === toolId)) {
setCustomTools(prev => prev.filter(t => t.id !== toolId));
} else {
@@ -145,7 +175,6 @@ export const AssistantsPage: React.FC = () => {
setIsAddToolModalOpen(true);
};
// Define tools available
const baseSystemTools: ToolItem[] = [
{ id: 'cam_open', name: '打开相机', icon: <Camera className="w-4 h-4" />, desc: '允许 AI 开启摄像头流', category: 'system' },
{ id: 'cam_close', name: '关闭相机', icon: <CameraOff className="w-4 h-4" />, desc: '允许 AI 停止摄像头流', category: 'system' },
@@ -163,11 +192,14 @@ export const AssistantsPage: React.FC = () => {
const systemTools = [...baseSystemTools, ...customTools.filter(t => t.category === 'system')].filter(t => !hiddenToolIds.includes(t.id));
const queryTools = [...baseQueryTools, ...customTools.filter(t => t.category === 'query')].filter(t => !hiddenToolIds.includes(t.id));
const isExternalConfig = selectedAssistant?.configMode === 'dify' || selectedAssistant?.configMode === 'fastgpt';
const isNoneConfig = selectedAssistant?.configMode === 'none' || !selectedAssistant?.configMode;
return (
<div className="flex h-[calc(100vh-6rem)] gap-6 animate-in fade-in">
<div className="flex h-[calc(100vh-8rem)] gap-6 animate-in fade-in py-4">
{/* LEFT COLUMN: List */}
<div className="w-80 flex flex-col gap-4 shrink-0">
<div className="flex items-center justify-between px-1">
<div className="flex items-center justify-between px-1 text-white">
<h2 className="text-xl font-bold tracking-tight text-white"></h2>
</div>
@@ -197,10 +229,24 @@ export const AssistantsPage: React.FC = () => {
: 'bg-card/30 border-white/5 hover:bg-white/5 hover:border-white/10'
}`}
>
<div className="flex justify-between items-start mb-2">
<span className={`font-semibold truncate pr-6 ${selectedId === assistant.id ? 'text-primary' : 'text-foreground'}`}>
<div className="flex flex-col gap-1.5 mb-2 pr-16 overflow-hidden">
<span className={`font-semibold truncate ${selectedId === assistant.id ? 'text-primary' : 'text-foreground'}`}>
{assistant.name}
</span>
{assistant.configMode && assistant.configMode !== 'none' && (
<div className="flex">
<Badge
variant="outline"
className={`text-[9px] uppercase tracking-tighter shrink-0 opacity-70 border-white/10 ${
assistant.configMode === 'platform' ? 'text-cyan-400 bg-cyan-400/5' :
assistant.configMode === 'dify' ? 'text-blue-400 bg-blue-400/5' :
'text-purple-400 bg-purple-400/5'
}`}
>
{assistant.configMode === 'platform' ? '内置' : assistant.configMode}
</Badge>
</div>
)}
</div>
<div className="flex items-center text-xs text-muted-foreground">
@@ -208,8 +254,7 @@ export const AssistantsPage: React.FC = () => {
<span>{assistant.callCount} </span>
</div>
{/* Hover Actions */}
<div className="absolute right-2 top-2 flex space-x-1 opacity-0 group-hover:opacity-100 transition-opacity bg-background/50 backdrop-blur-sm rounded-lg p-0.5">
<div className="absolute right-2 top-2 flex space-x-1 opacity-0 group-hover:opacity-100 transition-opacity bg-background/50 backdrop-blur-sm rounded-lg p-0.5 shadow-lg border border-white/5">
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={(e) => handleCopy(e, assistant)} title="复制">
<Copy className="h-3.5 w-3.5" />
</Button>
@@ -232,18 +277,29 @@ export const AssistantsPage: React.FC = () => {
{selectedAssistant ? (
<>
{/* Header Area */}
<div className="p-6 border-b border-white/5 bg-white/[0.02] space-y-4">
{/* Row 1: Name and Actions */}
<div className="flex items-end justify-between gap-4">
<div className="p-6 border-b border-white/5 bg-white/[0.02] space-y-5">
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<label className="text-[10px] text-muted-foreground font-black tracking-widest uppercase mb-2 block ml-1">ASSISTANT NAME</label>
<div className="flex items-center gap-2 mb-2">
<label className="text-[10px] text-muted-foreground font-black tracking-widest uppercase ml-1"></label>
<div className="flex items-center gap-1.5 px-2 py-0.5 rounded-md bg-white/5 border border-white/10 group/id transition-all hover:bg-white/10">
<span className="text-[10px] font-mono text-muted-foreground/60 select-all tracking-tight">UUID: {selectedAssistant.id}</span>
<button
onClick={() => handleCopyId(selectedAssistant.id)}
className="text-muted-foreground hover:text-primary transition-colors flex items-center"
title="复制ID"
>
{copySuccess ? <ClipboardCheck className="h-3 w-3 text-green-400" /> : <Copy className="h-3 w-3" />}
</button>
</div>
</div>
<Input
value={selectedAssistant.name}
onChange={(e) => updateAssistant('name', e.target.value)}
className="font-bold bg-white/5 border-white/10 focus:border-primary/50 text-base"
/>
</div>
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-2 pt-6">
<Button
variant="secondary"
onClick={() => setDebugOpen(true)}
@@ -252,7 +308,15 @@ export const AssistantsPage: React.FC = () => {
<Play className="mr-2 h-4 w-4" />
</Button>
<Button
onClick={() => alert("发布成功!")}
variant="outline"
onClick={handleSave}
disabled={saveLoading}
className="border border-white/10 hover:border-primary/50 text-foreground"
>
<Save className={`mr-2 h-4 w-4 ${saveLoading ? 'animate-pulse' : ''}`} /> {saveLoading ? '正在保存...' : '保存'}
</Button>
<Button
onClick={() => setIsPublishModalOpen(true)}
className="shadow-[0_0_20px_rgba(6,182,212,0.3)]"
>
<Rocket className="mr-2 h-4 w-4" />
@@ -260,33 +324,116 @@ export const AssistantsPage: React.FC = () => {
</div>
</div>
{/* Row 2: Tabs */}
<div className="flex bg-white/5 p-1 rounded-lg w-fit">
<button
onClick={() => setActiveTab(TabValue.GLOBAL)}
className={`px-6 py-1.5 text-sm font-medium rounded-md transition-all ${activeTab === TabValue.GLOBAL ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
>
</button>
<button
onClick={() => setActiveTab(TabValue.VOICE)}
className={`px-6 py-1.5 text-sm font-medium rounded-md transition-all ${activeTab === TabValue.VOICE ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
>
</button>
<button
onClick={() => setActiveTab(TabValue.TOOLS)}
className={`px-6 py-1.5 text-sm font-medium rounded-md transition-all ${activeTab === TabValue.TOOLS ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
>
</button>
<div className="space-y-2">
<label className="text-[10px] text-muted-foreground font-black tracking-widest uppercase ml-1"></label>
<div className="relative group w-full">
<select
className="flex h-10 w-full rounded-md border border-white/10 bg-white/5 px-3 py-1 text-sm shadow-sm transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 text-foreground appearance-none cursor-pointer [&>option]:bg-card"
value={selectedAssistant.configMode || 'none'}
onChange={(e) => updateAssistant('configMode', e.target.value as any)}
>
<option value="none"></option>
<option value="platform"></option>
<option value="dify">Dify </option>
<option value="fastgpt">FastGPT </option>
</select>
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none group-hover:text-primary transition-colors" />
</div>
</div>
{!isNoneConfig && (
<div className="flex bg-white/5 p-1 rounded-lg w-fit border border-white/5 animate-in fade-in slide-in-from-top-1">
{selectedAssistant.configMode === 'platform' ? (
<>
<button
onClick={() => setActiveTab(TabValue.GLOBAL)}
className={`px-6 py-1.5 text-sm font-medium rounded-md transition-all ${activeTab === TabValue.GLOBAL ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
>
</button>
<button
onClick={() => setActiveTab(TabValue.VOICE)}
className={`px-6 py-1.5 text-sm font-medium rounded-md transition-all ${activeTab === TabValue.VOICE ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
>
</button>
<button
onClick={() => setActiveTab(TabValue.TOOLS)}
className={`px-6 py-1.5 text-sm font-medium rounded-md transition-all ${activeTab === TabValue.TOOLS ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
>
</button>
</>
) : (
<>
<button
onClick={() => setActiveTab(TabValue.LINK)}
className={`px-6 py-1.5 text-sm font-medium rounded-md transition-all ${activeTab === TabValue.LINK ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
>
</button>
<button
onClick={() => setActiveTab(TabValue.VOICE)}
className={`px-6 py-1.5 text-sm font-medium rounded-md transition-all ${activeTab === TabValue.VOICE ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
>
</button>
</>
)}
</div>
)}
</div>
{/* Content Scroll Area */}
<div className="flex-1 overflow-y-auto p-8 custom-scrollbar">
<div className="max-w-4xl mx-auto space-y-8 animate-in slide-in-from-bottom-2 duration-300">
{activeTab === TabValue.GLOBAL && (
{isNoneConfig ? (
<div className="h-full flex flex-col items-center justify-center text-muted-foreground opacity-40 animate-in fade-in">
<AlertTriangle className="w-12 h-12 mb-4" />
<p className="text-sm font-medium"></p>
</div>
) : (
<div className="max-w-4xl mx-auto space-y-8 animate-in slide-in-from-bottom-2 duration-300">
{activeTab === TabValue.LINK && isExternalConfig && (
<div className="space-y-6 animate-in fade-in">
<div className="p-4 rounded-xl bg-primary/5 border border-primary/20 flex items-start gap-3 mb-4">
<Database className="w-5 h-5 text-primary shrink-0 mt-0.5" />
<div>
<h4 className="text-sm font-bold text-white mb-1">
{selectedAssistant.configMode === 'dify' ? 'Dify' : 'FastGPT'}
</h4>
<p className="text-xs text-muted-foreground leading-relaxed">
</p>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white flex items-center">
<Globe className="w-4 h-4 mr-2 text-primary"/> (API URL)
</label>
<Input
value={selectedAssistant.apiUrl || ''}
onChange={(e) => updateAssistant('apiUrl', e.target.value)}
placeholder={selectedAssistant.configMode === 'dify' ? "https://api.dify.ai/v1" : "https://api.fastgpt.in/api/v1"}
className="bg-white/5 border-white/10 focus:border-primary/50 font-mono text-xs"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white flex items-center">
<Terminal className="w-4 h-4 mr-2 text-primary"/> (API KEY)
</label>
<Input
type="password"
value={selectedAssistant.apiKey || ''}
onChange={(e) => updateAssistant('apiKey', e.target.value)}
placeholder="请输入应用 API 密钥..."
className="bg-white/5 border-white/10 focus:border-primary/50 font-mono text-xs"
/>
</div>
</div>
)}
{activeTab === TabValue.GLOBAL && selectedAssistant.configMode === 'platform' && (
<div className="space-y-6">
<div className="space-y-2">
<label className="text-sm font-medium text-white flex items-center">
@@ -329,57 +476,69 @@ export const AssistantsPage: React.FC = () => {
</div>
)}
{activeTab === TabValue.VOICE && (
{activeTab === TabValue.VOICE && !isNoneConfig && (
<div className="space-y-8">
<div className="grid grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-sm font-medium text-white"> (Language)</label>
<div className="space-y-2">
<label className="text-sm font-medium text-white flex items-center">
<Volume2 className="w-4 h-4 mr-2 text-primary"/> (From Voice Library)
</label>
<div className="relative group">
<select
className="flex h-10 w-full rounded-md border border-white/10 bg-white/5 px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card text-foreground"
value={selectedAssistant.language}
onChange={(e) => updateAssistant('language', e.target.value)}
>
<option value="zh"> (Chinese)</option>
<option value="en"> (English)</option>
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white"> (Voice)</label>
<select
className="flex h-10 w-full rounded-md border border-white/10 bg-white/5 px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card text-foreground"
className="flex h-12 w-full rounded-xl border border-white/10 bg-white/5 px-4 py-1 text-sm shadow-sm transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card text-foreground appearance-none cursor-pointer"
value={selectedAssistant.voice}
onChange={(e) => updateAssistant('voice', e.target.value)}
>
<option value="default"> (Default)</option>
<option value="alloy">Alloy</option>
<option value="echo">Echo</option>
<option value="fable">Fable</option>
<option value="onyx">Onyx</option>
<option value="nova">Nova</option>
<option value="shimmer">Shimmer</option>
<option value="" disabled>...</option>
{mockVoices.map(voice => (
<option key={voice.id} value={voice.id}>
{voice.name} ({voice.vendor} - {voice.gender === 'Male' ? '男' : '女'})
</option>
))}
</select>
<ChevronDown className="absolute right-4 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none group-hover:text-primary transition-colors" />
</div>
<p className="text-xs text-muted-foreground flex items-center mt-1">
<Sparkles className="w-3 h-3 mr-1 text-primary opacity-70" />
</p>
</div>
<div className="space-y-4 p-4 rounded-xl border border-white/5 bg-white/[0.02]">
<div className="flex justify-between items-center">
<label className="text-sm font-medium text-white"> (Speed)</label>
<span className="text-sm font-mono text-primary bg-primary/10 px-2 py-0.5 rounded">{selectedAssistant.speed}x</span>
<div className="space-y-4 pt-2">
<div className="flex justify-between items-center mb-1">
<label className="text-sm font-medium text-white flex items-center">
<Timer className="w-4 h-4 mr-2 text-primary"/> (Interruption Sensitivity)
</label>
<div className="flex items-center gap-2">
<div className="relative">
<Input
type="number"
value={selectedAssistant.interruptionSensitivity || 500}
onChange={(e) => updateAssistant('interruptionSensitivity', parseInt(e.target.value) || 0)}
className="w-20 h-8 text-right pr-7 text-xs font-mono bg-black/40 border-white/5"
/>
<span className="absolute right-2 top-1/2 -translate-y-1/2 text-[10px] text-muted-foreground font-mono">ms</span>
</div>
</div>
</div>
<input
type="range"
min="0.5"
max="2.0"
step="0.1"
value={selectedAssistant.speed}
onChange={(e) => updateAssistant('speed', parseFloat(e.target.value))}
className="w-full h-2 bg-secondary rounded-lg appearance-none cursor-pointer accent-primary"
/>
<div className="flex justify-between text-xs text-muted-foreground">
<span>0.5x (Slow)</span>
<span>1.0x (Normal)</span>
<span>2.0x (Fast)</span>
<div className="flex items-center gap-6">
<input
type="range"
min="0"
max="2000"
step="50"
value={selectedAssistant.interruptionSensitivity || 500}
onChange={(e) => updateAssistant('interruptionSensitivity', parseInt(e.target.value))}
className="flex-1 h-1.5 bg-secondary rounded-lg appearance-none cursor-pointer accent-primary"
/>
</div>
<div className="flex justify-between text-[10px] text-muted-foreground font-mono uppercase tracking-widest px-0.5 opacity-50">
<span>0ms (Extreme)</span>
<span>1000ms</span>
<span>2000ms (Lazy)</span>
</div>
<p className="text-xs text-muted-foreground pt-1 italic opacity-60">
* AI
</p>
</div>
<div className="space-y-3">
@@ -394,16 +553,16 @@ export const AssistantsPage: React.FC = () => {
onKeyDown={(e) => e.key === 'Enter' && addHotword()}
className="bg-white/5 border-white/10"
/>
<Button variant="secondary" onClick={addHotword}></Button>
<Button variant="secondary" onClick={addHotword} className="px-10 whitespace-nowrap"></Button>
</div>
<div className="flex flex-wrap gap-2 min-h-[40px] p-2 rounded-lg border border-dashed border-white/10">
{selectedAssistant.hotwords.length === 0 && (
<span className="text-xs text-muted-foreground py-1"></span>
<span className="text-xs text-muted-foreground py-1 px-1"></span>
)}
{selectedAssistant.hotwords.map((word, idx) => (
<Badge key={idx} variant="outline">
<Badge key={idx} variant="outline" className="py-1">
{word}
<button onClick={() => removeHotword(word)} className="ml-2 hover:text-destructive transition-colors">×</button>
<button onClick={() => removeHotword(word)} className="ml-2 hover:text-destructive transition-colors text-lg leading-none">×</button>
</Badge>
))}
</div>
@@ -412,7 +571,7 @@ export const AssistantsPage: React.FC = () => {
</div>
)}
{activeTab === TabValue.TOOLS && (
{activeTab === TabValue.TOOLS && selectedAssistant.configMode === 'platform' && (
<div className="space-y-8 animate-in fade-in">
<div className="space-y-4">
<div className="flex items-center justify-between">
@@ -445,7 +604,6 @@ export const AssistantsPage: React.FC = () => {
</div>
<p className="text-[10px] text-muted-foreground line-clamp-1 opacity-70">{tool.desc}</p>
</div>
{/* Delete Button */}
<button
onClick={(e) => deleteTool(e, tool.id)}
className="absolute -top-1 -right-1 p-0.5 rounded-full bg-destructive text-white opacity-0 group-hover:opacity-100 transition-opacity hover:scale-110 shadow-lg z-10"
@@ -489,7 +647,6 @@ export const AssistantsPage: React.FC = () => {
</div>
<p className="text-[10px] text-muted-foreground line-clamp-1 opacity-70">{tool.desc}</p>
</div>
{/* Delete Button */}
<button
onClick={(e) => deleteTool(e, tool.id)}
className="absolute -top-1 -right-1 p-0.5 rounded-full bg-destructive text-white opacity-0 group-hover:opacity-100 transition-opacity hover:scale-110 shadow-lg z-10"
@@ -507,7 +664,8 @@ export const AssistantsPage: React.FC = () => {
</div>
</div>
)}
</div>
</div>
)}
</div>
</>
) : (
@@ -521,6 +679,96 @@ export const AssistantsPage: React.FC = () => {
)}
</div>
{/* Publish Modal */}
<Dialog
isOpen={isPublishModalOpen}
onClose={() => setIsPublishModalOpen(false)}
title="发布小助手"
footer={
<Button onClick={() => setIsPublishModalOpen(false)}></Button>
}
>
<div className="space-y-6">
<div className="flex bg-white/5 p-1 rounded-lg border border-white/10">
<button
onClick={() => setPublishTab('web')}
className={`flex-1 flex items-center justify-center py-2 text-xs font-bold rounded-md transition-all ${publishTab === 'web' ? 'bg-primary text-primary-foreground shadow-lg' : 'text-muted-foreground hover:text-foreground'}`}
>
<ExternalLink className="w-3.5 h-3.5 mr-2" />
</button>
<button
onClick={() => setPublishTab('api')}
className={`flex-1 flex items-center justify-center py-2 text-xs font-bold rounded-md transition-all ${publishTab === 'api' ? 'bg-primary text-primary-foreground shadow-lg' : 'text-muted-foreground hover:text-foreground'}`}
>
<Server className="w-3.5 h-3.5 mr-2" /> API
</button>
</div>
{publishTab === 'web' ? (
<div className="space-y-4 animate-in fade-in slide-in-from-top-1">
<div className="p-4 rounded-xl bg-primary/5 border border-primary/20 space-y-3">
<div className="flex items-center gap-2 text-primary">
<Zap className="w-4 h-4" />
<h4 className="text-sm font-bold"></h4>
</div>
<p className="text-xs text-muted-foreground leading-relaxed">
</p>
</div>
<div className="space-y-2">
<label className="text-[10px] text-muted-foreground font-black tracking-widest uppercase">访</label>
<div className="flex gap-2">
<Input
readOnly
value={`https://ai-video.com/share/${selectedAssistant?.id}`}
className="bg-white/5 border-white/10 font-mono text-[11px] text-primary"
/>
<Button variant="secondary" size="icon" onClick={() => handleCopyId('', `https://ai-video.com/share/${selectedAssistant?.id}`)}>
{copySuccess ? <ClipboardCheck className="h-4 w-4 text-green-400" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
</div>
</div>
) : (
<div className="space-y-5 animate-in fade-in slide-in-from-top-1">
<div className="space-y-2">
<label className="text-[10px] text-muted-foreground font-black tracking-widest uppercase">API Endpoint (v1)</label>
<div className="flex gap-2">
<Input
readOnly
value={`https://api.ai-video.com/v1/call/${selectedAssistant?.id}`}
className="bg-white/5 border-white/10 font-mono text-[11px]"
/>
<Button variant="ghost" size="icon" onClick={() => handleCopyId('', `https://api.ai-video.com/v1/call/${selectedAssistant?.id}`)}>
<Copy className="h-4 w-4" />
</Button>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-[10px] text-muted-foreground font-black tracking-widest uppercase">Secret API KEY</label>
<Badge variant="outline" className="text-[8px] opacity-50">PRIVATE</Badge>
</div>
<div className="flex gap-2">
<Input
readOnly
type="password"
value="sk-ai-video-78x29jKkL1M90vX..."
className="bg-white/5 border-white/10 font-mono text-[11px]"
/>
<Button variant="ghost" size="icon" onClick={() => handleCopyId('', "sk-ai-video-78x29jKkL1M90vX...")}>
<Copy className="h-4 w-4" />
</Button>
</div>
<p className="text-[10px] text-muted-foreground italic flex items-center gap-1.5 px-1 mt-2">
<Key className="w-3 h-3" /> API Key
</p>
</div>
</div>
)}
</div>
</Dialog>
{selectedAssistant && (
<DebugDrawer
isOpen={debugOpen}
@@ -562,7 +810,7 @@ export const AssistantsPage: React.FC = () => {
</div>
<div className="p-3 bg-primary/5 border border-primary/20 rounded-lg text-[10px] text-muted-foreground flex items-start gap-2">
<Wrench className="w-3.5 h-3.5 text-primary shrink-0 mt-0.5" />
<p> AI these </p>
<p> and AI these </p>
</div>
</div>
</Dialog>