Improve web ui

This commit is contained in:
Xin Wang
2026-02-07 14:28:54 +08:00
parent dc3130d387
commit 2725d2fe20
10 changed files with 232 additions and 94 deletions

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { HashRouter as Router, Routes, Route, Link, useLocation } from 'react-router-dom'; import { HashRouter as Router, Routes, Route, Link, useLocation } from 'react-router-dom';
import { Bot, Book, User, LayoutDashboard, Mic2, Video, GitBranch, Zap, PanelLeftClose, PanelLeftOpen, History as HistoryIcon, ChevronDown, ChevronRight, Box, Wand2, Wrench, BrainCircuit, AudioLines } from 'lucide-react'; import { Bot, Book, User, LayoutDashboard, Mic2, Video, GitBranch, Zap, PanelLeftClose, PanelLeftOpen, History as HistoryIcon, ChevronDown, ChevronRight, Box, Wand2, Wrench, BrainCircuit, AudioLines, Activity } from 'lucide-react';
import { AssistantsPage } from './pages/Assistants'; import { AssistantsPage } from './pages/Assistants';
import { KnowledgeBasePage } from './pages/KnowledgeBase'; import { KnowledgeBasePage } from './pages/KnowledgeBase';
@@ -124,18 +124,32 @@ const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
{ path: '/workflows', label: '工作流', icon: <GitBranch className="h-5 w-5" /> }, { path: '/workflows', label: '工作流', icon: <GitBranch className="h-5 w-5" /> },
] ]
}, },
{
path: '#observation',
label: '观察记录',
icon: <Activity className="h-5 w-5" />,
children: [
{ path: '/history', label: '历史记录', icon: <HistoryIcon className="h-5 w-5" /> }, { path: '/history', label: '历史记录', icon: <HistoryIcon className="h-5 w-5" /> },
{ path: '/auto-test', label: '测试助手', icon: <Zap className="h-5 w-5" /> }, ]
},
{
path: '#testing',
label: '自动化测试',
icon: <Zap className="h-5 w-5" />,
children: [
{ path: '/auto-test', label: '测试助手', icon: <Bot className="h-5 w-5" /> },
]
},
{ {
path: '#components', path: '#components',
label: '组件库', label: '组件库',
icon: <Box className="h-5 w-5" />, icon: <Box className="h-5 w-5" />,
children: [ children: [
{ path: '/llms', label: '模型', icon: <BrainCircuit className="h-5 w-5" /> }, { path: '/llms', label: '模型接入', icon: <BrainCircuit className="h-5 w-5" /> },
{ path: '/asr', label: '语音识别', icon: <AudioLines className="h-5 w-5" /> }, { path: '/asr', label: '语音识别', icon: <AudioLines className="h-5 w-5" /> },
{ path: '/voices', label: '声音', icon: <Mic2 className="h-5 w-5" /> }, { path: '/voices', label: '声音资源', icon: <Mic2 className="h-5 w-5" /> },
{ path: '/knowledge', label: '知识库', icon: <Book className="h-5 w-5" /> }, { path: '/knowledge', label: '知识库', icon: <Book className="h-5 w-5" /> },
{ path: '/tools', label: '工具', icon: <Wrench className="h-5 w-5" /> }, { path: '/tools', label: '工具与插件', icon: <Wrench className="h-5 w-5" /> },
] ]
}, },
{ path: '/profile', label: '个人中心', icon: <User className="h-5 w-5" /> }, { path: '/profile', label: '个人中心', icon: <User className="h-5 w-5" /> },

View File

@@ -53,14 +53,23 @@ export const Input: React.FC<InputProps> = ({ className = '', ...props }) => {
}; };
// Card - Glassmorphism style, very subtle border // Card - Glassmorphism style, very subtle border
export const Card: React.FC<{ children: React.ReactNode; className?: string }> = ({ children, className = '' }) => ( interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
<div className={`rounded-xl border border-white/5 bg-card/40 backdrop-blur-md text-card-foreground shadow-sm ${className}`}> children: React.ReactNode;
className?: string;
}
export const Card: React.FC<CardProps> = ({ children, className = '', ...props }) => (
<div className={`rounded-xl border border-white/5 bg-card/40 backdrop-blur-md text-card-foreground shadow-sm ${className}`} {...props}>
{children} {children}
</div> </div>
); );
// Badge // Badge
export const Badge: React.FC<{ children: React.ReactNode; variant?: 'default' | 'success' | 'warning' | 'outline' }> = ({ children, variant = 'default' }) => { interface BadgeProps {
children: React.ReactNode;
variant?: 'default' | 'success' | 'warning' | 'outline';
className?: string;
}
export const Badge: React.FC<BadgeProps> = ({ children, variant = 'default', className = '' }) => {
const styles = { const styles = {
default: "border-transparent bg-primary/20 text-primary hover:bg-primary/30 border border-primary/20", default: "border-transparent bg-primary/20 text-primary hover:bg-primary/30 border border-primary/20",
success: "border-transparent bg-green-500/20 text-green-400 border border-green-500/20", success: "border-transparent bg-green-500/20 text-green-400 border border-green-500/20",
@@ -68,7 +77,7 @@ export const Badge: React.FC<{ children: React.ReactNode; variant?: 'default' |
outline: "text-foreground border border-white/10 hover:bg-accent hover:text-accent-foreground", outline: "text-foreground border border-white/10 hover:bg-accent hover:text-accent-foreground",
}; };
return ( return (
<div className={`inline-flex items-center rounded-md px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${styles[variant]}`}> <div className={`inline-flex items-center rounded-md px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${styles[variant]} ${className}`}>
{children} {children}
</div> </div>
); );
@@ -76,9 +85,24 @@ export const Badge: React.FC<{ children: React.ReactNode; variant?: 'default' |
// Table - Subtle borders // Table - Subtle borders
export const TableHeader: React.FC<{ children: React.ReactNode }> = ({ children }) => <thead className="[&_tr]:border-b [&_tr]:border-white/5">{children}</thead>; export const TableHeader: React.FC<{ children: React.ReactNode }> = ({ children }) => <thead className="[&_tr]:border-b [&_tr]:border-white/5">{children}</thead>;
export const TableRow: React.FC<{ children: React.ReactNode; className?: string }> = ({ children, className = '' }) => <tr className={`border-b border-white/5 transition-colors hover:bg-white/5 data-[state=selected]:bg-muted ${className}`}>{children}</tr>;
export const TableHead: React.FC<{ children: React.ReactNode; className?: string }> = ({ children, className = '' }) => <th className={`h-10 px-4 text-left align-middle text-sm font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 ${className}`}>{children}</th>; interface TableRowProps extends React.HTMLAttributes<HTMLTableRowElement> {
export const TableCell: React.FC<{ children: React.ReactNode; className?: string }> = ({ children, className = '' }) => <td className={`p-4 align-middle text-sm [&:has([role=checkbox])]:pr-0 ${className}`}>{children}</td>; children: React.ReactNode;
className?: string;
}
export const TableRow: React.FC<TableRowProps> = ({ children, className = '', ...props }) => <tr className={`border-b border-white/5 transition-colors hover:bg-white/5 data-[state=selected]:bg-muted ${className}`} {...props}>{children}</tr>;
interface TableHeadProps extends React.ThHTMLAttributes<HTMLTableCellElement> {
children: React.ReactNode;
className?: string;
}
export const TableHead: React.FC<TableHeadProps> = ({ children, className = '', ...props }) => <th className={`h-10 px-4 text-left align-middle text-sm font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 ${className}`} {...props}>{children}</th>;
interface TableCellProps extends React.TdHTMLAttributes<HTMLTableCellElement> {
children: React.ReactNode;
className?: string;
}
export const TableCell: React.FC<TableCellProps> = ({ children, className = '', ...props }) => <td className={`p-4 align-middle text-sm [&:has([role=checkbox])]:pr-0 ${className}`} {...props}>{children}</td>;
// Drawer (Side Sheet) // Drawer (Side Sheet)
interface DrawerProps { interface DrawerProps {

View File

@@ -1,8 +1,8 @@
import React, { useState, useEffect, useRef } from 'react'; 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, ClipboardCheck, Sparkles, Volume2, Timer, ChevronDown, Link as LinkIcon, Database, Server, Zap, ExternalLink, Key } 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, BrainCircuit, Ear, Book, Filter } from 'lucide-react';
import { Button, Input, Card, Badge, Drawer, Dialog } from '../components/UI'; import { Button, Input, Card, Badge, Drawer, Dialog } from '../components/UI';
import { mockAssistants, mockKnowledgeBases, mockVoices } from '../services/mockData'; import { mockAssistants, mockKnowledgeBases, mockVoices, mockLLMModels, mockASRModels } from '../services/mockData';
import { Assistant, TabValue } from '../types'; import { Assistant, TabValue } from '../types';
import { GoogleGenAI } from "@google/genai"; import { GoogleGenAI } from "@google/genai";
@@ -63,6 +63,10 @@ export const AssistantsPage: React.FC = () => {
tools: [], tools: [],
interruptionSensitivity: 500, interruptionSensitivity: 500,
configMode: 'platform', configMode: 'platform',
llmModelId: '',
asrModelId: '',
embeddingModelId: '',
rerankModelId: '',
}; };
setAssistants([...assistants, newAssistant]); setAssistants([...assistants, newAssistant]);
setSelectedId(newId); setSelectedId(newId);
@@ -363,6 +367,12 @@ export const AssistantsPage: React.FC = () => {
> >
</button> </button>
<button
onClick={() => setActiveTab(TabValue.KNOWLEDGE)}
className={`px-6 py-1.5 text-sm font-medium rounded-md transition-all ${activeTab === TabValue.KNOWLEDGE ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
>
</button>
</> </>
) : ( ) : (
<> <>
@@ -435,6 +445,26 @@ export const AssistantsPage: React.FC = () => {
{activeTab === TabValue.GLOBAL && selectedAssistant.configMode === 'platform' && ( {activeTab === TabValue.GLOBAL && selectedAssistant.configMode === 'platform' && (
<div className="space-y-6"> <div className="space-y-6">
<div className="space-y-2">
<label className="text-sm font-medium text-white flex items-center">
<BrainCircuit className="w-4 h-4 mr-2 text-primary"/> (LLM Model)
</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 appearance-none cursor-pointer"
value={selectedAssistant.llmModelId || ''}
onChange={(e) => updateAssistant('llmModelId', e.target.value)}
>
<option value="">使</option>
{mockLLMModels.filter(m => m.type === 'text').map(model => (
<option key={model.id} value={model.id}>{model.name} ({model.vendor})</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>
<p className="text-xs text-muted-foreground"></p>
</div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium text-white flex items-center"> <label className="text-sm font-medium text-white flex items-center">
<MessageSquare className="w-4 h-4 mr-2 text-primary"/> (Opener) <MessageSquare className="w-4 h-4 mr-2 text-primary"/> (Opener)
@@ -459,11 +489,58 @@ export const AssistantsPage: React.FC = () => {
placeholder="设定小助手的人设、语气、行为规范以及业务逻辑..." placeholder="设定小助手的人设、语气、行为规范以及业务逻辑..."
/> />
</div> </div>
</div>
)}
{activeTab === TabValue.KNOWLEDGE && selectedAssistant.configMode === 'platform' && (
<div className="space-y-8 animate-in fade-in">
<div className="space-y-2">
<label className="text-sm font-medium text-white flex items-center">
<BrainCircuit className="w-4 h-4 mr-2 text-primary"/> (Embedding Model)
</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 appearance-none cursor-pointer"
value={selectedAssistant.embeddingModelId || ''}
onChange={(e) => updateAssistant('embeddingModelId', e.target.value)}
>
<option value="">使</option>
{mockLLMModels.filter(m => m.type === 'embedding').map(model => (
<option key={model.id} value={model.id}>{model.name} ({model.vendor})</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>
<p className="text-xs text-muted-foreground"></p>
</div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium text-white"></label> <label className="text-sm font-medium text-white flex items-center">
<Filter className="w-4 h-4 mr-2 text-primary"/> (Rerank Model)
</label>
<div className="relative group">
<select <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-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 appearance-none cursor-pointer"
value={selectedAssistant.rerankModelId || ''}
onChange={(e) => updateAssistant('rerankModelId', e.target.value)}
>
<option value="">使</option>
{mockLLMModels.filter(m => m.type === 'rerank').map(model => (
<option key={model.id} value={model.id}>{model.name} ({model.vendor})</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>
<p className="text-xs text-muted-foreground"></p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white flex items-center">
<Book className="w-4 h-4 mr-2 text-primary"/> (Knowledge Base)
</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 appearance-none cursor-pointer"
value={selectedAssistant.knowledgeBaseId} value={selectedAssistant.knowledgeBaseId}
onChange={(e) => updateAssistant('knowledgeBaseId', e.target.value)} onChange={(e) => updateAssistant('knowledgeBaseId', e.target.value)}
> >
@@ -472,12 +549,39 @@ export const AssistantsPage: React.FC = () => {
<option key={kb.id} value={kb.id}>{kb.name}</option> <option key={kb.id} value={kb.id}>{kb.name}</option>
))} ))}
</select> </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>
<p className="text-xs text-muted-foreground"></p>
</div> </div>
</div> </div>
)} )}
{activeTab === TabValue.VOICE && !isNoneConfig && ( {activeTab === TabValue.VOICE && !isNoneConfig && (
<div className="space-y-8"> <div className="space-y-8">
<div className="space-y-2">
<label className="text-sm font-medium text-white flex items-center">
<Ear className="w-4 h-4 mr-2 text-primary"/> (ASR Model)
</label>
<div className="relative group">
<select
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.asrModelId || ''}
onChange={(e) => updateAssistant('asrModelId', e.target.value)}
>
<option value="">使</option>
{mockASRModels.map(model => (
<option key={model.id} value={model.id}>
{model.name} ({model.vendor})
</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">
</p>
</div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium text-white flex items-center"> <label className="text-sm font-medium text-white flex items-center">
<Volume2 className="w-4 h-4 mr-2 text-primary"/> (From Voice Library) <Volume2 className="w-4 h-4 mr-2 text-primary"/> (From Voice Library)

View File

@@ -41,7 +41,7 @@ export const CallLogsPage: React.FC = () => {
return ( return (
<div className="space-y-6 animate-in fade-in py-2 pb-10"> <div className="space-y-6 animate-in fade-in py-2 pb-10">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-2xl font-bold tracking-tight"></h1> <h1 className="text-2xl font-bold tracking-tight text-white"></h1>
<Button variant="outline" onClick={handleExport}> <Button variant="outline" onClick={handleExport}>
<Download className="mr-2 h-4 w-4" /> <Download className="mr-2 h-4 w-4" />
</Button> </Button>
@@ -117,12 +117,7 @@ export const CallLogsPage: React.FC = () => {
))} ))}
{filteredLogs.length === 0 && ( {filteredLogs.length === 0 && (
<TableRow> <TableRow>
<TableCell className="text-center py-6 text-muted-foreground"></TableCell> <TableCell colSpan={6} className="text-center py-6 text-muted-foreground"></TableCell>
<TableCell> </TableCell>
<TableCell> </TableCell>
<TableCell> </TableCell>
<TableCell> </TableCell>
<TableCell> </TableCell>
</TableRow> </TableRow>
)} )}
</tbody> </tbody>

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Download, Search, Calendar, Filter, MessageSquare, Mic, Video, Eye, X, Play } from 'lucide-react'; import { Download, Search, Calendar, Filter, MessageSquare, Mic, Video, Eye, X, Play, User, Bot, Clock } from 'lucide-react';
import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Badge, Drawer } from '../components/UI'; import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Badge, Drawer } from '../components/UI';
import { mockCallLogs } from '../services/mockData'; import { mockCallLogs } from '../services/mockData';
import { CallLog, InteractionType } from '../types'; import { CallLog, InteractionType } from '../types';
@@ -66,7 +66,7 @@ export const HistoryPage: React.FC = () => {
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Filter className="h-4 w-4 text-muted-foreground" /> <Filter className="h-4 w-4 text-muted-foreground" />
<select <select
className="flex h-9 w-full rounded-md border-0 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" className="flex h-9 w-full rounded-md border-0 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={sourceFilter} value={sourceFilter}
onChange={(e) => setSourceFilter(e.target.value as any)} onChange={(e) => setSourceFilter(e.target.value as any)}
> >
@@ -77,7 +77,7 @@ export const HistoryPage: React.FC = () => {
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<select <select
className="flex h-9 w-full rounded-md border-0 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" className="flex h-9 w-full rounded-md border-0 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={typeFilter} value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value as any)} onChange={(e) => setTypeFilter(e.target.value as any)}
> >
@@ -89,7 +89,7 @@ export const HistoryPage: React.FC = () => {
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<select <select
className="flex h-9 w-full rounded-md border-0 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" className="flex h-9 w-full rounded-md border-0 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={statusFilter} value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as any)} onChange={(e) => setStatusFilter(e.target.value as any)}
> >
@@ -157,7 +157,7 @@ export const HistoryPage: React.FC = () => {
<Drawer <Drawer
isOpen={!!selectedLog} isOpen={!!selectedLog}
onClose={() => setSelectedLog(null)} onClose={() => setSelectedLog(null)}
title="历史记录详情" title="通话详情与对话记录"
> >
<div className="flex flex-col h-full overflow-hidden"> <div className="flex flex-col h-full overflow-hidden">
<div className="shrink-0 mb-4 p-4 bg-white/5 rounded-xl border border-white/10 space-y-3"> <div className="shrink-0 mb-4 p-4 bg-white/5 rounded-xl border border-white/10 space-y-3">
@@ -173,68 +173,62 @@ export const HistoryPage: React.FC = () => {
</div> </div>
</div> </div>
<div className="flex-1 overflow-y-auto space-y-4 pr-1 custom-scrollbar pb-6"> <div className="flex-1 overflow-y-auto space-y-6 pr-2 custom-scrollbar pb-6 px-1">
{(selectedLog.details && selectedLog.details.length > 0) ? ( {(selectedLog.details && selectedLog.details.length > 0) ? (
selectedLog.details.map((detail, index) => ( selectedLog.details.map((detail, index) => (
<div key={index} className={`flex flex-col gap-1 ${detail.role === 'user' ? 'items-end' : 'items-start'}`}> <div key={index} className={`flex gap-3 ${detail.role === 'user' ? 'flex-row-reverse' : 'flex-row'}`}>
<div className={`max-w-[85%] rounded-2xl p-4 shadow-sm border ${ <div className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 border border-white/10 ${detail.role === 'user' ? 'bg-primary/20 text-primary' : 'bg-white/5 text-muted-foreground'}`}>
detail.role === 'user' {detail.role === 'user' ? <User className="w-4 h-4" /> : <Bot className="w-4 h-4" />}
? 'bg-primary/10 border-primary/20 rounded-tr-none'
: 'bg-card border-white/10 rounded-tl-none'
}`}>
<div className="flex items-center gap-2 mb-2 opacity-70">
<span className="text-[10px] font-bold uppercase tracking-wider text-primary">
{detail.role === 'user' ? 'User' : 'AI Assistant'}
</span>
<span className="text-[10px] text-muted-foreground">{detail.timestamp}</span>
</div> </div>
<div className={`flex flex-col max-w-[85%] ${detail.role === 'user' ? 'items-end' : 'items-start'}`}>
<div className="flex items-center gap-2 mb-1 opacity-60">
<span className="text-[10px] uppercase font-bold tracking-wider">{detail.role === 'user' ? 'User' : 'Assistant'}</span>
<span className="text-[10px] font-mono">{detail.timestamp}</span>
</div>
<div className={`rounded-2xl p-3 shadow-sm text-sm leading-relaxed border ${
detail.role === 'user'
? 'bg-primary/10 border-primary/20 text-white rounded-tr-none'
: 'bg-card border-white/10 text-white/90 rounded-tl-none'
}`}>
{/* Video Frames */} {/* Video Frames */}
{selectedLog.type === 'video' && detail.role === 'user' && detail.imageUrls && detail.imageUrls.length > 0 && ( {selectedLog.type === 'video' && detail.role === 'user' && detail.imageUrls && detail.imageUrls.length > 0 && (
<div className="flex gap-2 overflow-x-auto mb-3 pb-2"> <div className="flex gap-2 overflow-x-auto mb-2 pb-1 custom-scrollbar">
{detail.imageUrls.map((url, i) => ( {detail.imageUrls.map((url, i) => (
<div key={i} className="relative h-20 w-32 rounded-lg overflow-hidden border border-white/10 bg-black/50 shrink-0 group"> <div key={i} className="relative h-16 w-24 rounded-lg overflow-hidden border border-white/10 bg-black/50 shrink-0 group">
<img src={url} alt={`Frame ${i}`} className="h-full w-full object-cover" /> <img src={url} alt={`Frame ${i}`} className="h-full w-full object-cover" />
<div className="absolute inset-0 flex items-center justify-center bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity"> <div className="absolute inset-0 flex items-center justify-center bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity">
<Eye className="w-5 h-5 text-white" /> <Eye className="w-4 h-4 text-white" />
</div> </div>
</div> </div>
))} ))}
</div> </div>
)} )}
{/* Content / Transcript */} {/* Content */}
<div className="text-sm leading-relaxed text-white/90"> <div>{detail.content}</div>
{selectedLog.type !== 'text' && (
<div className="flex items-center gap-2 mb-1.5">
<div className={`p-1.5 rounded-full ${detail.role === 'user' ? 'bg-primary/20' : 'bg-white/10'}`}>
{selectedLog.type === 'audio' ? <Mic size={12} /> : <Video size={12} />}
</div>
<span className="text-[10px] uppercase font-mono text-muted-foreground">Transcript</span>
</div>
)}
{detail.content}
</div>
{/* Audio Player Placeholder for Audio/Video types */} {/* Audio Player Placeholder */}
{selectedLog.type !== 'text' && ( {selectedLog.type !== 'text' && (
<div className="mt-3 flex items-center gap-2 p-2 rounded-lg bg-black/20 border border-white/5"> <div className="mt-2 flex items-center gap-2 p-1.5 rounded-lg bg-black/20 border border-white/5 w-full min-w-[160px]">
<button className="w-6 h-6 rounded-full bg-white/10 hover:bg-primary hover:text-white flex items-center justify-center transition-colors"> <button className="w-5 h-5 rounded-full bg-white/10 hover:bg-primary hover:text-white flex items-center justify-center transition-colors shrink-0">
<Play size={10} className="ml-0.5" /> <Play size={8} className="ml-0.5" />
</button> </button>
<div className="h-1 flex-1 bg-white/10 rounded-full overflow-hidden"> <div className="h-1 flex-1 bg-white/10 rounded-full overflow-hidden">
<div className="h-full w-1/3 bg-primary/50"></div> <div className="h-full w-1/3 bg-primary/50"></div>
</div> </div>
<span className="text-[10px] font-mono text-muted-foreground">00:05</span> <span className="text-[9px] font-mono text-muted-foreground">00:05</span>
</div> </div>
)} )}
</div> </div>
</div> </div>
</div>
)) ))
) : ( ) : (
<div className="h-full flex flex-col items-center justify-center text-muted-foreground opacity-50 space-y-2"> <div className="h-full flex flex-col items-center justify-center text-muted-foreground opacity-40 space-y-3">
<MessageSquare className="w-10 h-10" /> <MessageSquare className="w-12 h-12 stroke-1" />
<p className="text-sm"></p> <p className="text-sm font-medium"></p>
</div> </div>
)} )}
</div> </div>

View File

@@ -56,7 +56,7 @@ export const LLMLibraryPage: React.FC = () => {
return ( return (
<div className="space-y-6 animate-in fade-in py-4 pb-10"> <div className="space-y-6 animate-in fade-in py-4 pb-10">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-2xl font-bold tracking-tight text-white"></h1> <h1 className="text-2xl font-bold tracking-tight text-white"></h1>
<Button onClick={() => setIsAddModalOpen(true)} className="shadow-[0_0_15px_rgba(6,182,212,0.4)]"> <Button onClick={() => setIsAddModalOpen(true)} className="shadow-[0_0_15px_rgba(6,182,212,0.4)]">
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
</Button> </Button>

View File

@@ -63,7 +63,7 @@ export const ToolLibraryPage: React.FC = () => {
return ( return (
<div className="space-y-6 animate-in fade-in py-4 pb-10"> <div className="space-y-6 animate-in fade-in py-4 pb-10">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-2xl font-bold tracking-tight text-white"></h1> <h1 className="text-2xl font-bold tracking-tight text-white"></h1>
<Button onClick={() => setIsAddModalOpen(true)} className="shadow-[0_0_15px_rgba(6,182,212,0.4)]"> <Button onClick={() => setIsAddModalOpen(true)} className="shadow-[0_0_15px_rgba(6,182,212,0.4)]">
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
</Button> </Button>

View File

@@ -44,7 +44,7 @@ export const VoiceLibraryPage: React.FC = () => {
return ( return (
<div className="space-y-6 animate-in fade-in py-4 pb-10"> <div className="space-y-6 animate-in fade-in py-4 pb-10">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-2xl font-bold tracking-tight text-white"></h1> <h1 className="text-2xl font-bold tracking-tight text-white"></h1>
<div className="flex space-x-3"> <div className="flex space-x-3">
<Button variant="primary" onClick={() => setIsAddModalOpen(true)} className="shadow-[0_0_15px_rgba(6,182,212,0.4)]"> <Button variant="primary" onClick={() => setIsAddModalOpen(true)} className="shadow-[0_0_15px_rgba(6,182,212,0.4)]">
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />

View File

@@ -22,6 +22,7 @@ export const mockAssistants: Assistant[] = [
speed: 1.0, speed: 1.0,
hotwords: ['refund', 'order'], hotwords: ['refund', 'order'],
interruptionSensitivity: 500, interruptionSensitivity: 500,
embeddingModelId: 'm3',
}, },
{ {
id: '2', id: '2',
@@ -199,6 +200,7 @@ export const mockLLMModels: LLMModel[] = [
{ id: 'm1', name: 'GPT-4o', vendor: 'OpenAI Compatible', type: 'text', baseUrl: 'https://api.openai.com/v1', apiKey: 'sk-***', temperature: 0.7 }, { id: 'm1', name: 'GPT-4o', vendor: 'OpenAI Compatible', type: 'text', baseUrl: 'https://api.openai.com/v1', apiKey: 'sk-***', temperature: 0.7 },
{ id: 'm2', name: 'DeepSeek-V3', vendor: 'OpenAI Compatible', type: 'text', baseUrl: 'https://api.deepseek.com', apiKey: 'sk-***', temperature: 0.5 }, { id: 'm2', name: 'DeepSeek-V3', vendor: 'OpenAI Compatible', type: 'text', baseUrl: 'https://api.deepseek.com', apiKey: 'sk-***', temperature: 0.5 },
{ id: 'm3', name: 'text-embedding-3-small', vendor: 'OpenAI Compatible', type: 'embedding', baseUrl: 'https://api.openai.com/v1', apiKey: 'sk-***' }, { id: 'm3', name: 'text-embedding-3-small', vendor: 'OpenAI Compatible', type: 'embedding', baseUrl: 'https://api.openai.com/v1', apiKey: 'sk-***' },
{ id: 'm4', name: 'bge-reranker-v2-m3', vendor: 'SiliconFlow', type: 'rerank', baseUrl: 'https://api.siliconflow.cn/v1', apiKey: 'sk-***' },
]; ];
export const mockASRModels: ASRModel[] = [ export const mockASRModels: ASRModel[] = [

View File

@@ -15,6 +15,10 @@ export interface Assistant {
configMode?: 'platform' | 'dify' | 'fastgpt' | 'none'; configMode?: 'platform' | 'dify' | 'fastgpt' | 'none';
apiUrl?: string; apiUrl?: string;
apiKey?: string; apiKey?: string;
llmModelId?: string;
asrModelId?: string;
embeddingModelId?: string;
rerankModelId?: string;
} }
export interface Voice { export interface Voice {
@@ -116,6 +120,7 @@ export enum TabValue {
GLOBAL = 'global', GLOBAL = 'global',
VOICE = 'voice', VOICE = 'voice',
TOOLS = 'tools', TOOLS = 'tools',
KNOWLEDGE = 'knowledge',
LINK = 'link' LINK = 'link'
} }