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:
gongzhiyong
2026-04-08 17:34:23 +08:00
co-authored by Claude Sonnet 4.6
parent 433d2361a8
commit 71efdff800
3 changed files with 61 additions and 49 deletions
+39 -39
View File
@@ -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>
);
+10 -10
View File
@@ -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) {
+12
View File
@@ -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 {