Switch TicketSummary to use /api/tickets/summary endpoint
- Add fetchTicketSummary() and TicketSummaryData type to lib/api.ts - Change TicketSummary props from tickets[] array to summary object with total, by_status, and by_priority fields - Update GeminiChat to fetch summary data instead of ticket list - TicketSummary now renders status and priority breakdowns from the summary endpoint instead of computing from raw ticket data Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
433d2361a8
commit
71efdff800
@@ -251,30 +251,28 @@ export function ExtensionsPanel({
|
||||
|
||||
// 工单摘要组件 - 显示在对话区域
|
||||
interface TicketSummaryProps {
|
||||
tickets: {
|
||||
id: string;
|
||||
title: string;
|
||||
status: "pending" | "processing" | "resolved";
|
||||
priority: "P0" | "P1" | "P2" | "P3";
|
||||
createdAt: string;
|
||||
}[];
|
||||
summary: {
|
||||
total: number;
|
||||
by_status: { pending: number; processing: number; resolved: number };
|
||||
by_priority: { P0: number; P1: number; P2: number; P3: number };
|
||||
};
|
||||
}
|
||||
|
||||
export function TicketSummary({ tickets }: TicketSummaryProps) {
|
||||
const statusMap = {
|
||||
pending: { label: "待处理", color: "text-yellow-400 bg-yellow-900/30" },
|
||||
processing: { label: "处理中", color: "text-blue-400 bg-blue-900/30" },
|
||||
resolved: { label: "已解决", color: "text-green-400 bg-green-900/30" },
|
||||
};
|
||||
export function TicketSummary({ summary }: TicketSummaryProps) {
|
||||
const statusEntries: { key: keyof typeof summary.by_status; label: string; color: string }[] = [
|
||||
{ key: "pending", label: "待处理", color: "text-yellow-400 bg-yellow-900/30" },
|
||||
{ key: "processing", label: "处理中", color: "text-blue-400 bg-blue-900/30" },
|
||||
{ key: "resolved", label: "已解决", color: "text-green-400 bg-green-900/30" },
|
||||
];
|
||||
|
||||
const priorityColors = {
|
||||
P0: "text-red-400",
|
||||
P1: "text-orange-400",
|
||||
P2: "text-yellow-400",
|
||||
P3: "text-[var(--gem-text-muted)]",
|
||||
};
|
||||
const priorityEntries: { key: keyof typeof summary.by_priority; color: string }[] = [
|
||||
{ key: "P0", color: "text-red-400" },
|
||||
{ key: "P1", color: "text-orange-400" },
|
||||
{ key: "P2", color: "text-yellow-400" },
|
||||
{ key: "P3", color: "text-[var(--gem-text-muted)]" },
|
||||
];
|
||||
|
||||
if (tickets.length === 0) return null;
|
||||
if (summary.total === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl px-4 mb-4">
|
||||
@@ -282,39 +280,41 @@ export function TicketSummary({ tickets }: TicketSummaryProps) {
|
||||
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-[var(--gem-border)] bg-[var(--gem-surface-hover)]">
|
||||
<Ticket size={16} className="text-[var(--gem-blue)]" />
|
||||
<span className="text-sm font-medium text-[var(--gem-text)]">工单摘要</span>
|
||||
<span className="text-xs text-[var(--gem-text-muted)]">({tickets.length} 条待关注)</span>
|
||||
<span className="text-xs text-[var(--gem-text-muted)]">({summary.total} 条待关注)</span>
|
||||
</div>
|
||||
<div className="divide-y divide-[var(--gem-surface-2)]">
|
||||
{tickets.slice(0, 5).map((ticket) => (
|
||||
{statusEntries.map(({ key, label, color }) => (
|
||||
<div
|
||||
key={ticket.id}
|
||||
key={key}
|
||||
className="flex items-center gap-3 px-4 py-2.5 hover:bg-[var(--gem-surface-2)] transition-colors cursor-pointer"
|
||||
>
|
||||
<span className={cn("text-xs font-mono", priorityColors[ticket.priority])}>
|
||||
{ticket.priority}
|
||||
</span>
|
||||
<span className="flex-1 text-sm text-[var(--gem-text)] truncate">
|
||||
{ticket.title}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs px-2 py-0.5 rounded-full",
|
||||
statusMap[ticket.status].color
|
||||
color
|
||||
)}
|
||||
>
|
||||
{statusMap[ticket.status].label}
|
||||
{label}
|
||||
</span>
|
||||
<span className="flex-1 text-sm text-[var(--gem-text)]">
|
||||
{summary.by_status[key]} 条
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{priorityEntries.map(({ key, color }) => (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center gap-3 px-4 py-2.5 hover:bg-[var(--gem-surface-2)] transition-colors cursor-pointer"
|
||||
>
|
||||
<span className={cn("text-xs font-mono", color)}>
|
||||
{key}
|
||||
</span>
|
||||
<span className="flex-1 text-sm text-[var(--gem-text)]">
|
||||
{summary.by_priority[key]} 条
|
||||
</span>
|
||||
<span className="text-xs text-[var(--gem-text-placeholder)]">{ticket.createdAt}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{tickets.length > 5 && (
|
||||
<div className="px-4 py-2 text-center border-t border-[var(--gem-surface-2)]">
|
||||
<button className="text-xs text-[var(--gem-blue)] hover:underline">
|
||||
查看全部 {tickets.length} 条工单
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,9 +13,9 @@ import { cn } from "@/lib/utils";
|
||||
import {
|
||||
fetchConversations,
|
||||
fetchConversation,
|
||||
fetchTickets,
|
||||
fetchTicketSummary,
|
||||
streamChat,
|
||||
type TicketData,
|
||||
type TicketSummaryData,
|
||||
} from "@/lib/api";
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────────────
|
||||
@@ -70,7 +70,7 @@ export function GeminiChat() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [extensionsPanelOpen, setExtensionsPanelOpen] = useState(false);
|
||||
const [extensions, setExtensions] = useState<Extension[]>(INITIAL_EXTENSIONS);
|
||||
const [tickets, setTickets] = useState<TicketData[]>([]);
|
||||
const [ticketSummary, setTicketSummary] = useState<TicketSummaryData | null>(null);
|
||||
const [activeTools, setActiveTools] = useState<Set<string>>(new Set());
|
||||
const [selectedModel, setSelectedModel] = useState<"flash" | "pro">("pro");
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
@@ -95,14 +95,14 @@ export function GeminiChat() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Load tickets when ticket extension connects
|
||||
// Load ticket summary when ticket extension connects
|
||||
useEffect(() => {
|
||||
if (ticketSystemConnected) {
|
||||
fetchTickets()
|
||||
.then(setTickets)
|
||||
.catch(() => setTickets([]));
|
||||
fetchTicketSummary()
|
||||
.then(setTicketSummary)
|
||||
.catch(() => setTicketSummary(null));
|
||||
} else {
|
||||
setTickets([]);
|
||||
setTicketSummary(null);
|
||||
}
|
||||
}, [ticketSystemConnected]);
|
||||
|
||||
@@ -348,12 +348,12 @@ export function GeminiChat() {
|
||||
<>
|
||||
<GeminiWelcome onSuggestionClick={handleSuggestionClick} />
|
||||
{/* 显示工单摘要(仅当工单系统已连接) */}
|
||||
{ticketSystemConnected && <TicketSummary tickets={tickets} />}
|
||||
{ticketSystemConnected && ticketSummary && <TicketSummary summary={ticketSummary} />}
|
||||
</>
|
||||
) : (
|
||||
<div className="max-w-3xl mx-auto px-4 pt-8 pb-4">
|
||||
{/* 对话中也显示工单摘要 */}
|
||||
{ticketSystemConnected && <TicketSummary tickets={tickets} />}
|
||||
{ticketSystemConnected && ticketSummary && <TicketSummary summary={ticketSummary} />}
|
||||
{messages.map((msg) => {
|
||||
// Hide empty assistant message while streaming — the typing indicator covers it
|
||||
if (msg.role === "assistant" && msg.content === "" && isLoading) {
|
||||
|
||||
@@ -63,6 +63,18 @@ export async function fetchTickets(page = 1, pageSize = 20): Promise<TicketData[
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export interface TicketSummaryData {
|
||||
total: number;
|
||||
by_status: { pending: number; processing: number; resolved: number };
|
||||
by_priority: { P0: number; P1: number; P2: number; P3: number };
|
||||
}
|
||||
|
||||
export async function fetchTicketSummary(): Promise<TicketSummaryData> {
|
||||
const res = await fetch(`${API_URL}/api/tickets/summary`);
|
||||
if (!res.ok) throw new Error("Failed to fetch ticket summary");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// ── SSE Chat Stream ──────────────────────────────────────────────────────────
|
||||
|
||||
export interface ChatStreamEvent {
|
||||
|
||||
Reference in New Issue
Block a user